@hue-run/sdk 0.11.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +3 -3
- package/EVALUATIONS.md +25 -9
- package/dist/cli/eval.js +17 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +36 -24
- package/dist/evals/client.d.ts +25 -5
- package/dist/evals/client.js +93 -15
- package/dist/evals/environment-target.d.ts +15 -8
- package/dist/evals/environment-target.js +61 -21
- package/dist/evals/local-worker.d.ts +38 -3
- package/dist/evals/local-worker.js +158 -34
- package/dist/evals/runner.d.ts +24 -0
- package/dist/evals/runner.js +154 -12
- package/dist/evals.d.ts +3 -3
- package/dist/evals.js +2 -2
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +76 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.12.0`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -469,7 +469,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
469
469
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
470
470
|
project=/absolute/path/to/supported-fixture
|
|
471
471
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
472
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
472
|
+
--archive .artifacts/typescript/hue-run-sdk-0.12.0.tgz \
|
|
473
473
|
--origin https://STAGING_ORIGIN \
|
|
474
474
|
--project "$project" --command setup \
|
|
475
475
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -480,7 +480,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
480
480
|
|
|
481
481
|
```sh
|
|
482
482
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
483
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
483
|
+
--archive .artifacts/typescript/hue-run-sdk-0.12.0.tgz \
|
|
484
484
|
--origin https://STAGING_ORIGIN \
|
|
485
485
|
--project "$project" --command claim \
|
|
486
486
|
--evidence .context/setup-staging-after-claim.json
|
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 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.
|
|
120
120
|
|
|
121
121
|
## Local scorers
|
|
122
122
|
|
|
@@ -252,14 +252,30 @@ without invoking the target or scorers. A lost preparation acknowledgement remai
|
|
|
252
252
|
and is never recovered through binding reads, credential refresh or target replay. Synthetic
|
|
253
253
|
acceptance does not contact official Gmail or claim universal provider parity.
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
255
|
+
The worker does not die with one failure. A read, or a mutation the server deduplicates by its
|
|
256
|
+
idempotency key (the worker's own register, claim, heartbeat and completion included), is sent
|
|
257
|
+
again after a connection failure, a timeout or a 408 or 5xx that carried no `Retry-After`, up to
|
|
258
|
+
`maxAttempts` (default 4) times; a mutation without a key is sent once. A registration or claim
|
|
259
|
+
that still fails transiently is reported through `onPollError` and polled again with a growing
|
|
260
|
+
wait; a refusal Hue decided on (a revoked key, a disabled agent) is thrown. A transient failure
|
|
261
|
+
of a completion or result upload keeps the run claimed by the durable worker identity: the next
|
|
262
|
+
claim returns it to the same process, which resumes saved uploads without invoking the candidate
|
|
263
|
+
again (`onRunFailed` says `retry`), and after `maxRunAttempts` (default 5) such failures the run
|
|
264
|
+
is given up as `attention` for a project member to requeue or cancel from its run page; a failure
|
|
265
|
+
that would recur (a refusal Hue decided on, a case file whose bytes differ from its manifest) is
|
|
266
|
+
given up at once. A lost
|
|
267
|
+
world-seal acknowledgement is recovered by reading authoritative world state. If the seal or
|
|
268
|
+
candidate outcome cannot be confirmed, or an outcome cannot be serialized, the run is given up
|
|
269
|
+
as `attention` at once and the worker goes on to other runs; when Hue cannot record that either,
|
|
270
|
+
the worker stops with the error. Presenting the same uncertain checkpoint again cannot replay the
|
|
271
|
+
candidate. `maxRuns` counts runs that settled either way. Inside a run, one case's own failure
|
|
272
|
+
does not stop the cases beside it; a failure every case would share (Hue not answering or
|
|
273
|
+
refusing, telemetry not accepted, a world that could not be sealed) stops new cases from
|
|
274
|
+
starting until the run resumes. Each world's lifetime is `worldTtlSeconds` (the server's default
|
|
275
|
+
is an hour): a target still running past it is told through `context.signal`, its case ends as
|
|
276
|
+
an `error` of type `TargetTimeout` and the world is sealed abandoned. Public package acceptance
|
|
277
|
+
proves this lifecycle against local fixtures; exact installed-registry-package to hosted-facade
|
|
278
|
+
acceptance remains a post-publication Fern gate.
|
|
263
279
|
|
|
264
280
|
## Direct cases and files
|
|
265
281
|
|
package/dist/cli/eval.js
CHANGED
|
@@ -1302,6 +1302,20 @@ async function runWorker(values, connection, adapter, agent, hue, output, signal
|
|
|
1302
1302
|
deprecationWarnings: false,
|
|
1303
1303
|
signal,
|
|
1304
1304
|
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
1305
|
+
onPollError(error) {
|
|
1306
|
+
output.error(`Hue did not answer the poll (${explain(error)}); polling again`);
|
|
1307
|
+
},
|
|
1308
|
+
onRunFailed(failure) {
|
|
1309
|
+
const run = `Run ${failure.runId}`;
|
|
1310
|
+
if (failure.outcome === "retry") {
|
|
1311
|
+
const waitMillis = failure.waitMillis ?? 0;
|
|
1312
|
+
output.error(`${run} attempt ${failure.attempt} failed (${explain(failure.error)}); resuming in ${Math.round(waitMillis / 1000)} s`);
|
|
1313
|
+
}
|
|
1314
|
+
else if (failure.outcome === "attention")
|
|
1315
|
+
output.error(`${run} needs attention (${explain(failure.error)}); requeue or cancel it from its run page`);
|
|
1316
|
+
else
|
|
1317
|
+
output.error(`${run} was released or cancelled in Hue; its outcome here is not recorded`);
|
|
1318
|
+
},
|
|
1305
1319
|
target(inputs, tools, context) {
|
|
1306
1320
|
output.log(`[${context.item.externalKey}] agent started`);
|
|
1307
1321
|
return adapter(inputs, {
|
|
@@ -1315,7 +1329,9 @@ async function runWorker(values, connection, adapter, agent, hue, output, signal
|
|
|
1315
1329
|
...(context.connectionBundle ? { connectionBundle: context.connectionBundle } : {}),
|
|
1316
1330
|
files: context.files,
|
|
1317
1331
|
outputDirectory: context.outputDirectory,
|
|
1318
|
-
signal,
|
|
1332
|
+
// The case's own signal: the worker stopping, or the world's deadline passing, both
|
|
1333
|
+
// end a command that would otherwise keep running past its timed-out case.
|
|
1334
|
+
signal: context.signal ?? signal,
|
|
1319
1335
|
});
|
|
1320
1336
|
},
|
|
1321
1337
|
async onCompleted(report) {
|
package/dist/client.d.ts
CHANGED
|
@@ -43,6 +43,9 @@ export declare class HueClient {
|
|
|
43
43
|
readonly enabled: boolean;
|
|
44
44
|
private logger;
|
|
45
45
|
private storage;
|
|
46
|
+
/** Reports a contained capture or instrumentation failure, named to the trace active where
|
|
47
|
+
* it happened when there is one. */
|
|
48
|
+
private instrumentationFailed;
|
|
46
49
|
private tracerProvider;
|
|
47
50
|
private loggerProvider;
|
|
48
51
|
private ownedProviders?;
|
package/dist/client.js
CHANGED
|
@@ -169,6 +169,20 @@ export class HueClient {
|
|
|
169
169
|
enabled;
|
|
170
170
|
logger;
|
|
171
171
|
storage = new AsyncLocalStorage();
|
|
172
|
+
/** Reports a contained capture or instrumentation failure, named to the trace active where
|
|
173
|
+
* it happened when there is one. */
|
|
174
|
+
instrumentationFailed(signal = "traces", count = 1) {
|
|
175
|
+
let traceId;
|
|
176
|
+
try {
|
|
177
|
+
traceId = trace
|
|
178
|
+
.getSpan(this.storage.getStore()?.context ?? context.active())
|
|
179
|
+
?.spanContext().traceId;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// The failure is recorded without a trace.
|
|
183
|
+
}
|
|
184
|
+
this.transport.instrumentationFailure(signal, undefined, count, traceId);
|
|
185
|
+
}
|
|
172
186
|
tracerProvider;
|
|
173
187
|
loggerProvider;
|
|
174
188
|
ownedProviders;
|
|
@@ -210,7 +224,7 @@ export class HueClient {
|
|
|
210
224
|
}
|
|
211
225
|
this.captureContent = this.transport.options.captureContent;
|
|
212
226
|
this.enabled = this.transport.options.enabled !== false;
|
|
213
|
-
this.tracer = new ContextualTracer(this.tracerProvider.getTracer(HUE_SCOPE, sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.
|
|
227
|
+
this.tracer = new ContextualTracer(this.tracerProvider.getTracer(HUE_SCOPE, sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.instrumentationFailed());
|
|
214
228
|
this.logger = this.loggerProvider.getLogger(HUE_SCOPE, sdkVersion);
|
|
215
229
|
}
|
|
216
230
|
/**
|
|
@@ -232,7 +246,7 @@ export class HueClient {
|
|
|
232
246
|
return this.storage.getStore()?.context ?? context.active();
|
|
233
247
|
}
|
|
234
248
|
catch {
|
|
235
|
-
this.
|
|
249
|
+
this.instrumentationFailed();
|
|
236
250
|
return ROOT_CONTEXT;
|
|
237
251
|
}
|
|
238
252
|
}
|
|
@@ -256,7 +270,7 @@ export class HueClient {
|
|
|
256
270
|
span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
|
|
257
271
|
}
|
|
258
272
|
catch {
|
|
259
|
-
this.
|
|
273
|
+
this.instrumentationFailed();
|
|
260
274
|
}
|
|
261
275
|
}
|
|
262
276
|
// A disabled or failed client creates no span; the application's own active span then stays
|
|
@@ -268,7 +282,7 @@ export class HueClient {
|
|
|
268
282
|
spanContext = trace.setSpan(active.context, span);
|
|
269
283
|
}
|
|
270
284
|
catch {
|
|
271
|
-
this.
|
|
285
|
+
this.instrumentationFailed();
|
|
272
286
|
spanContext = trace.setSpan(ROOT_CONTEXT, span);
|
|
273
287
|
}
|
|
274
288
|
const handle = {
|
|
@@ -287,7 +301,7 @@ export class HueClient {
|
|
|
287
301
|
handle.setInput(options.input);
|
|
288
302
|
}
|
|
289
303
|
catch {
|
|
290
|
-
this.
|
|
304
|
+
this.instrumentationFailed();
|
|
291
305
|
}
|
|
292
306
|
try {
|
|
293
307
|
return await callback(handle);
|
|
@@ -303,9 +317,7 @@ export class HueClient {
|
|
|
303
317
|
// The span is also OpenTelemetry's active span while the callback runs, so spans from other
|
|
304
318
|
// instrumentations (HTTP clients, provider SDKs) join this trace when the application has
|
|
305
319
|
// registered a context manager. Hue still registers none itself.
|
|
306
|
-
return this.storage.run({ ...active, context: spanContext }, () => created
|
|
307
|
-
? runInContext(spanContext, execute, () => this.transport.instrumentationFailure())
|
|
308
|
-
: execute());
|
|
320
|
+
return this.storage.run({ ...active, context: spanContext }, () => created ? runInContext(spanContext, execute, () => this.instrumentationFailed()) : execute());
|
|
309
321
|
}
|
|
310
322
|
/**
|
|
311
323
|
* Runs `execute` inside an `execute_tool` span named after the tool. `input` and a defined result
|
|
@@ -329,7 +341,7 @@ export class HueClient {
|
|
|
329
341
|
if (isLabel(value))
|
|
330
342
|
attributes[key] = value;
|
|
331
343
|
else if (this.enabled && !this.closed)
|
|
332
|
-
this.
|
|
344
|
+
this.instrumentationFailed();
|
|
333
345
|
};
|
|
334
346
|
stamp("gen_ai.tool.call.id", options.callId);
|
|
335
347
|
stamp("mcp.server.name", options.mcp?.name);
|
|
@@ -364,7 +376,7 @@ export class HueClient {
|
|
|
364
376
|
if (isLabel(value))
|
|
365
377
|
return value;
|
|
366
378
|
if (active)
|
|
367
|
-
this.
|
|
379
|
+
this.instrumentationFailed();
|
|
368
380
|
return fallback;
|
|
369
381
|
};
|
|
370
382
|
const requestModel = label(model, "unknown");
|
|
@@ -413,14 +425,14 @@ export class HueClient {
|
|
|
413
425
|
if (value === undefined)
|
|
414
426
|
continue;
|
|
415
427
|
if (!Number.isInteger(value) || value < 0) {
|
|
416
|
-
this.
|
|
428
|
+
this.instrumentationFailed();
|
|
417
429
|
continue;
|
|
418
430
|
}
|
|
419
431
|
try {
|
|
420
432
|
span.setAttribute(key, value);
|
|
421
433
|
}
|
|
422
434
|
catch {
|
|
423
|
-
this.
|
|
435
|
+
this.instrumentationFailed();
|
|
424
436
|
}
|
|
425
437
|
}
|
|
426
438
|
}
|
|
@@ -433,7 +445,7 @@ export class HueClient {
|
|
|
433
445
|
propagator.inject(activeContext, carrier, defaultTextMapSetter);
|
|
434
446
|
}
|
|
435
447
|
catch {
|
|
436
|
-
this.
|
|
448
|
+
this.instrumentationFailed();
|
|
437
449
|
}
|
|
438
450
|
}
|
|
439
451
|
/** Reads W3C trace context from a carrier for use as `parentContext`. */
|
|
@@ -442,7 +454,7 @@ export class HueClient {
|
|
|
442
454
|
return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
|
|
443
455
|
}
|
|
444
456
|
catch {
|
|
445
|
-
this.
|
|
457
|
+
this.instrumentationFailed();
|
|
446
458
|
return ROOT_CONTEXT;
|
|
447
459
|
}
|
|
448
460
|
}
|
|
@@ -461,7 +473,7 @@ export class HueClient {
|
|
|
461
473
|
span.addEvent("exception", { "exception.type": type });
|
|
462
474
|
}
|
|
463
475
|
catch {
|
|
464
|
-
this.
|
|
476
|
+
this.instrumentationFailed();
|
|
465
477
|
}
|
|
466
478
|
}
|
|
467
479
|
/**
|
|
@@ -498,7 +510,7 @@ export class HueClient {
|
|
|
498
510
|
else if (typeof explicit === "string" && explicit.trim() && explicit.length <= 256)
|
|
499
511
|
attributes[key] = explicit;
|
|
500
512
|
else
|
|
501
|
-
this.
|
|
513
|
+
this.instrumentationFailed("logs");
|
|
502
514
|
};
|
|
503
515
|
stamp("gen_ai.operation.name", messages.operation, enclosing?.model?.operation);
|
|
504
516
|
stamp("gen_ai.provider.name", messages.provider, enclosing?.model?.provider);
|
|
@@ -513,7 +525,7 @@ export class HueClient {
|
|
|
513
525
|
});
|
|
514
526
|
}
|
|
515
527
|
catch {
|
|
516
|
-
this.
|
|
528
|
+
this.instrumentationFailed("logs");
|
|
517
529
|
}
|
|
518
530
|
}
|
|
519
531
|
/**
|
|
@@ -545,7 +557,7 @@ export class HueClient {
|
|
|
545
557
|
const addresses = hostedServerAddresses(provider, options.request);
|
|
546
558
|
const activity = hostedToolActivity(provider, response);
|
|
547
559
|
if (activity.skipped > 0)
|
|
548
|
-
this.
|
|
560
|
+
this.instrumentationFailed("traces", activity.skipped);
|
|
549
561
|
const server = (label) => {
|
|
550
562
|
const attributes = {};
|
|
551
563
|
if (label === undefined)
|
|
@@ -564,7 +576,7 @@ export class HueClient {
|
|
|
564
576
|
if (isLabel(value))
|
|
565
577
|
attributes[key] = value;
|
|
566
578
|
else
|
|
567
|
-
this.
|
|
579
|
+
this.instrumentationFailed();
|
|
568
580
|
}
|
|
569
581
|
const address = addresses.get(label);
|
|
570
582
|
if (address !== undefined)
|
|
@@ -617,7 +629,7 @@ export class HueClient {
|
|
|
617
629
|
span.setAttributes(toolCatalogSummary(encodeContent(listing.definitions, catalogLimits)));
|
|
618
630
|
}
|
|
619
631
|
catch {
|
|
620
|
-
this.
|
|
632
|
+
this.instrumentationFailed();
|
|
621
633
|
}
|
|
622
634
|
if (listing.errorType !== undefined)
|
|
623
635
|
fail(span, listing.errorType);
|
|
@@ -625,7 +637,7 @@ export class HueClient {
|
|
|
625
637
|
}
|
|
626
638
|
}
|
|
627
639
|
catch {
|
|
628
|
-
this.
|
|
640
|
+
this.instrumentationFailed();
|
|
629
641
|
}
|
|
630
642
|
}
|
|
631
643
|
/**
|
|
@@ -692,12 +704,12 @@ export class HueClient {
|
|
|
692
704
|
if (isLabel(name))
|
|
693
705
|
attributes["hue.file.name"] = name;
|
|
694
706
|
else
|
|
695
|
-
this.
|
|
707
|
+
this.instrumentationFailed();
|
|
696
708
|
}
|
|
697
709
|
span.addEvent("hue.file", attributes);
|
|
698
710
|
}
|
|
699
711
|
catch {
|
|
700
|
-
this.
|
|
712
|
+
this.instrumentationFailed();
|
|
701
713
|
}
|
|
702
714
|
}
|
|
703
715
|
setContent(span, key, value) {
|
|
@@ -707,7 +719,7 @@ export class HueClient {
|
|
|
707
719
|
span.setAttribute(key, encodeContent(value));
|
|
708
720
|
}
|
|
709
721
|
catch {
|
|
710
|
-
this.
|
|
722
|
+
this.instrumentationFailed();
|
|
711
723
|
}
|
|
712
724
|
}
|
|
713
725
|
/**
|
package/dist/evals/client.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface EvaluationClientOptions {
|
|
|
9
9
|
baseUrl?: string;
|
|
10
10
|
/** Per-request budget in milliseconds, 100–60000. Default 10000. */
|
|
11
11
|
timeoutMillis?: number;
|
|
12
|
+
/** Bounded attempts for a read or a mutation the server deduplicates by its key, after a
|
|
13
|
+
* connection failure, a timeout or a 408 or 5xx Hue did not time with `Retry-After`. Default 4,
|
|
14
|
+
* maximum 10; 1 sends every such request once. */
|
|
15
|
+
maxAttempts?: number;
|
|
12
16
|
}
|
|
13
17
|
/** Thrown for a failed evaluation API request; the message is fixed and never includes response text. */
|
|
14
18
|
export declare class HueApiError extends Error {
|
|
@@ -16,12 +20,22 @@ export declare class HueApiError extends Error {
|
|
|
16
20
|
readonly status?: number | undefined;
|
|
17
21
|
/** Seconds Hue asked the caller to wait (`Retry-After` on a 429 or 503), when it said. */
|
|
18
22
|
readonly retryAfterSeconds?: number | undefined;
|
|
23
|
+
/** Hue's `X-Hue-Diagnostic` code, which tells refusals of one status apart, when it sent one. */
|
|
24
|
+
readonly diagnostic?: string | undefined;
|
|
19
25
|
constructor(
|
|
20
26
|
/** HTTP status when Hue answered; absent for network, timeout and parsing failures. */
|
|
21
27
|
status?: number | undefined,
|
|
22
28
|
/** Seconds Hue asked the caller to wait (`Retry-After` on a 429 or 503), when it said. */
|
|
23
|
-
retryAfterSeconds?: number | undefined
|
|
29
|
+
retryAfterSeconds?: number | undefined,
|
|
30
|
+
/** Hue's `X-Hue-Diagnostic` code, which tells refusals of one status apart, when it sent one. */
|
|
31
|
+
diagnostic?: string | undefined);
|
|
24
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the same request, sent again, can succeed: a connection failure, a timeout, or a status
|
|
35
|
+
* Hue answers while it cannot act yet (408, 429, 500, 502, 503, 504). A refusal Hue decided on,
|
|
36
|
+
* any other 4xx, is not, and no number of attempts changes it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isTransientApiError(error: unknown): boolean;
|
|
25
39
|
/** An artifact download that ran past the size its caller expected; the rest is not read. */
|
|
26
40
|
export declare class ArtifactSizeError extends Error {
|
|
27
41
|
/** The size the caller expected, in bytes. */
|
|
@@ -32,22 +46,28 @@ export declare class ArtifactSizeError extends Error {
|
|
|
32
46
|
}
|
|
33
47
|
/**
|
|
34
48
|
* Typed client for Hue's evaluation REST API: datasets, scorers, experiments, executions, runs,
|
|
35
|
-
* results and hosted judge jobs.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
49
|
+
* results and hosted judge jobs. A request Hue refused before acting on it with a short
|
|
50
|
+
* `Retry-After` (HTTP 429 or 503, at most 5 seconds) is sent again after that wait whatever its
|
|
51
|
+
* method, up to four times; a longer wait is the caller's error, carried as `retryAfterSeconds`.
|
|
52
|
+
* A read, or a mutation the server deduplicates by its idempotency key, is also sent again after
|
|
53
|
+
* a failure Hue did not time, a connection failure, a timeout or a 408 or 5xx without the header,
|
|
54
|
+
* up to `maxAttempts` times with a jittered backoff. A mutation without a key is never retried
|
|
55
|
+
* implicitly: callers retain stable idempotency keys for experiments and results. Responses are
|
|
56
|
+
* bounded to 4 MiB.
|
|
39
57
|
*/
|
|
40
58
|
export declare class EvaluationClient {
|
|
41
59
|
/** Validated Hue origin. */
|
|
42
60
|
readonly baseUrl: string;
|
|
43
61
|
private readonly apiKey;
|
|
44
62
|
private readonly timeoutMillis;
|
|
63
|
+
private readonly maxAttempts;
|
|
45
64
|
constructor(options: EvaluationClientOptions);
|
|
46
65
|
/** Sends one request to Hue, again after the wait while Hue refuses it with a short
|
|
47
66
|
* `Retry-After`. `init` runs for every attempt, so each has its own timeout; `signal` also ends
|
|
48
67
|
* each attempt, and a wait to send one again, whose refusal is then the caller's error. */
|
|
49
68
|
private send;
|
|
50
69
|
private request;
|
|
70
|
+
private requestOnce;
|
|
51
71
|
/** Verified bytes of one ready artifact in this project, bounded to the 25 MiB pilot file size.
|
|
52
72
|
* With `maxBytes`, the download stops one byte past it and throws `ArtifactSizeError`. */
|
|
53
73
|
downloadArtifact(id: string, options?: {
|
package/dist/evals/client.js
CHANGED
|
@@ -5,17 +5,51 @@ import { attemptBindingRead, parsePrepareAttemptResultV2, parseRefreshedAttemptR
|
|
|
5
5
|
export class HueApiError extends Error {
|
|
6
6
|
status;
|
|
7
7
|
retryAfterSeconds;
|
|
8
|
+
diagnostic;
|
|
8
9
|
constructor(
|
|
9
10
|
/** HTTP status when Hue answered; absent for network, timeout and parsing failures. */
|
|
10
11
|
status,
|
|
11
12
|
/** Seconds Hue asked the caller to wait (`Retry-After` on a 429 or 503), when it said. */
|
|
12
|
-
retryAfterSeconds
|
|
13
|
-
|
|
13
|
+
retryAfterSeconds,
|
|
14
|
+
/** Hue's `X-Hue-Diagnostic` code, which tells refusals of one status apart, when it sent one. */
|
|
15
|
+
diagnostic) {
|
|
16
|
+
super(status
|
|
17
|
+
? `Hue API request failed (HTTP ${status}${diagnostic ? `, ${diagnostic}` : ""})`
|
|
18
|
+
: "Hue API connection or response failed");
|
|
14
19
|
this.status = status;
|
|
15
20
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
21
|
+
this.diagnostic = diagnostic;
|
|
16
22
|
this.name = "HueApiError";
|
|
17
23
|
}
|
|
18
24
|
}
|
|
25
|
+
const TRANSIENT = new Set([408, 429, 500, 502, 503, 504]);
|
|
26
|
+
/**
|
|
27
|
+
* Whether the same request, sent again, can succeed: a connection failure, a timeout, or a status
|
|
28
|
+
* Hue answers while it cannot act yet (408, 429, 500, 502, 503, 504). A refusal Hue decided on,
|
|
29
|
+
* any other 4xx, is not, and no number of attempts changes it.
|
|
30
|
+
*/
|
|
31
|
+
export function isTransientApiError(error) {
|
|
32
|
+
return (error instanceof HueApiError && (error.status === undefined || TRANSIENT.has(error.status)));
|
|
33
|
+
}
|
|
34
|
+
const DIAGNOSTIC = /^[a-z_]{1,64}$/;
|
|
35
|
+
function diagnosticOf(response) {
|
|
36
|
+
const value = response.headers.get("x-hue-diagnostic");
|
|
37
|
+
return value !== null && DIAGNOSTIC.test(value) ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Whether a request may be sent again after a transient failure: every read, and a mutation the
|
|
40
|
+
* server deduplicates, either by the `idempotencyKey` in its body or because its route is
|
|
41
|
+
* idempotent by design (the local worker's register, claim, heartbeat, completion and capability
|
|
42
|
+
* routes). A mutation without a key is sent once; its caller resolves the outcome before asking
|
|
43
|
+
* again. */
|
|
44
|
+
function retryable(method, body, idempotent) {
|
|
45
|
+
if (idempotent !== undefined)
|
|
46
|
+
return idempotent;
|
|
47
|
+
if (method === "GET")
|
|
48
|
+
return true;
|
|
49
|
+
return (body !== null &&
|
|
50
|
+
typeof body === "object" &&
|
|
51
|
+
typeof body.idempotencyKey === "string");
|
|
52
|
+
}
|
|
19
53
|
/** An artifact download that ran past the size its caller expected; the rest is not read. */
|
|
20
54
|
export class ArtifactSizeError extends Error {
|
|
21
55
|
maxBytes;
|
|
@@ -128,16 +162,21 @@ function refusalRetryAfter(response) {
|
|
|
128
162
|
}
|
|
129
163
|
/**
|
|
130
164
|
* Typed client for Hue's evaluation REST API: datasets, scorers, experiments, executions, runs,
|
|
131
|
-
* results and hosted judge jobs.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
165
|
+
* results and hosted judge jobs. A request Hue refused before acting on it with a short
|
|
166
|
+
* `Retry-After` (HTTP 429 or 503, at most 5 seconds) is sent again after that wait whatever its
|
|
167
|
+
* method, up to four times; a longer wait is the caller's error, carried as `retryAfterSeconds`.
|
|
168
|
+
* A read, or a mutation the server deduplicates by its idempotency key, is also sent again after
|
|
169
|
+
* a failure Hue did not time, a connection failure, a timeout or a 408 or 5xx without the header,
|
|
170
|
+
* up to `maxAttempts` times with a jittered backoff. A mutation without a key is never retried
|
|
171
|
+
* implicitly: callers retain stable idempotency keys for experiments and results. Responses are
|
|
172
|
+
* bounded to 4 MiB.
|
|
135
173
|
*/
|
|
136
174
|
export class EvaluationClient {
|
|
137
175
|
/** Validated Hue origin. */
|
|
138
176
|
baseUrl;
|
|
139
177
|
apiKey;
|
|
140
178
|
timeoutMillis;
|
|
179
|
+
maxAttempts;
|
|
141
180
|
constructor(options) {
|
|
142
181
|
const validated = validateOptions({
|
|
143
182
|
...options,
|
|
@@ -147,6 +186,10 @@ export class EvaluationClient {
|
|
|
147
186
|
this.baseUrl = validated.baseUrl;
|
|
148
187
|
this.apiKey = validated.apiKey;
|
|
149
188
|
this.timeoutMillis = validated.timeoutMillis;
|
|
189
|
+
const attempts = options.maxAttempts ?? 4;
|
|
190
|
+
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
|
|
191
|
+
throw new RangeError("maxAttempts must be 1–10");
|
|
192
|
+
this.maxAttempts = attempts;
|
|
150
193
|
}
|
|
151
194
|
/** Sends one request to Hue, again after the wait while Hue refuses it with a short
|
|
152
195
|
* `Retry-After`. `init` runs for every attempt, so each has its own timeout; `signal` also ends
|
|
@@ -183,11 +226,44 @@ export class EvaluationClient {
|
|
|
183
226
|
throw new HueApiError(response.status, askedRetryAfter(response));
|
|
184
227
|
}
|
|
185
228
|
}
|
|
186
|
-
async request(method, path, body, bounds = { ...valueBounds, bytes: 1024 * 1024 }, signal
|
|
187
|
-
|
|
229
|
+
async request(method, path, body, bounds = { ...valueBounds, bytes: 1024 * 1024 }, signal,
|
|
230
|
+
/** Overrides the retry decision `retryable` takes from the method and body. */
|
|
231
|
+
idempotent) {
|
|
232
|
+
// Serialized once, outside the attempts: a body this client cannot encode is a caller error
|
|
233
|
+
// no attempt fixes. Optional top-level fields are omitted intentionally; nested undefined
|
|
234
|
+
// remains invalid.
|
|
188
235
|
const payload = body === undefined
|
|
189
236
|
? undefined
|
|
190
237
|
: JSON.stringify(json(Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined)), bounds));
|
|
238
|
+
const attempts = retryable(method, body, idempotent) ? this.maxAttempts : 1;
|
|
239
|
+
for (let attempt = 1;; attempt++) {
|
|
240
|
+
try {
|
|
241
|
+
return await this.requestOnce(method, path, payload, signal);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
// A refusal that carried `Retry-After` was already decided by `send`: sent again while
|
|
245
|
+
// the wait was short, the caller's error when it was long. This loop covers the failures
|
|
246
|
+
// Hue did not time: a lost connection, a timeout, a 5xx or 408 without the header.
|
|
247
|
+
if (!isTransientApiError(error) ||
|
|
248
|
+
error.retryAfterSeconds !== undefined ||
|
|
249
|
+
attempt >= attempts ||
|
|
250
|
+
signal?.aborted)
|
|
251
|
+
throw error;
|
|
252
|
+
const backoff = Math.min(100 * 2 ** (attempt - 1), 2000);
|
|
253
|
+
const wait = backoff + Math.random() * backoff;
|
|
254
|
+
await new Promise((resolve) => {
|
|
255
|
+
const done = () => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
signal?.removeEventListener("abort", done);
|
|
258
|
+
resolve();
|
|
259
|
+
};
|
|
260
|
+
const timer = setTimeout(done, wait);
|
|
261
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async requestOnce(method, path, payload, signal) {
|
|
191
267
|
const response = await this.send(`${this.baseUrl}/api/v1${path}`, () => ({
|
|
192
268
|
method,
|
|
193
269
|
headers: {
|
|
@@ -200,7 +276,7 @@ export class EvaluationClient {
|
|
|
200
276
|
}), signal);
|
|
201
277
|
if (!response.ok) {
|
|
202
278
|
await response.body?.cancel();
|
|
203
|
-
throw new HueApiError(response.status, askedRetryAfter(response));
|
|
279
|
+
throw new HueApiError(response.status, askedRetryAfter(response), diagnosticOf(response));
|
|
204
280
|
}
|
|
205
281
|
try {
|
|
206
282
|
const reader = response.body?.getReader();
|
|
@@ -474,7 +550,9 @@ export class EvaluationClient {
|
|
|
474
550
|
async prepareAttempt(input) {
|
|
475
551
|
const request = prepareAttemptInputV2.parse(input);
|
|
476
552
|
const { executionId, ...body } = request;
|
|
477
|
-
|
|
553
|
+
// Sent once although keyed: a lost acknowledgement of a credential-bearing decision stays
|
|
554
|
+
// uncertain, and the worker never reacquires credentials.
|
|
555
|
+
const response = await this.request("POST", `/experiment-executions/${executionId}/prepare-attempt`, body, { ...valueBounds, bytes: 128_000 }, undefined, false);
|
|
478
556
|
try {
|
|
479
557
|
return parsePrepareAttemptResultV2(response, request);
|
|
480
558
|
}
|
|
@@ -662,21 +740,21 @@ export class EvaluationClient {
|
|
|
662
740
|
}
|
|
663
741
|
/** Register or refresh the fixed local agent key and revision. */
|
|
664
742
|
async registerLocalAgent(input) {
|
|
665
|
-
const agent = await this.request("POST", "/local-agent-worker/register", input);
|
|
743
|
+
const agent = await this.request("POST", "/local-agent-worker/register", input, undefined, undefined, true);
|
|
666
744
|
// Hue's response names the key `agentKey`; read either name.
|
|
667
745
|
return { ...agent, key: agent.key ?? agent.agentKey ?? input.key };
|
|
668
746
|
}
|
|
669
747
|
/** Claim a queued run for this agent and durable worker identity. */
|
|
670
748
|
claimLocalAgentRun(input) {
|
|
671
|
-
return this.request("POST", "/local-agent-worker/claim", input);
|
|
749
|
+
return this.request("POST", "/local-agent-worker/claim", input, undefined, undefined, true);
|
|
672
750
|
}
|
|
673
751
|
/** Refresh the lease of a claimed local run. */
|
|
674
752
|
heartbeatLocalAgentRun(input) {
|
|
675
|
-
return this.request("POST", "/local-agent-worker/runs/heartbeat", input);
|
|
753
|
+
return this.request("POST", "/local-agent-worker/runs/heartbeat", input, undefined, undefined, true);
|
|
676
754
|
}
|
|
677
755
|
/** Report acknowledged completion or an execution requiring attention. */
|
|
678
756
|
completeLocalAgentRun(input) {
|
|
679
|
-
return this.request("POST", "/local-agent-worker/runs/complete", input);
|
|
757
|
+
return this.request("POST", "/local-agent-worker/runs/complete", input, undefined, undefined, true);
|
|
680
758
|
}
|
|
681
759
|
/** Lists Scenarios (draft and published) of the project; requires a Read and write key. */
|
|
682
760
|
listCaseConversions(page) {
|
|
@@ -688,7 +766,7 @@ export class EvaluationClient {
|
|
|
688
766
|
}
|
|
689
767
|
/** Creates the legacy execution-scoped generic MCP capability for one world. */
|
|
690
768
|
createSimulationMcpCapability(input) {
|
|
691
|
-
return this.request("POST", "/local-agent-worker/mcp-capability", input);
|
|
769
|
+
return this.request("POST", "/local-agent-worker/mcp-capability", input, undefined, undefined, true);
|
|
692
770
|
}
|
|
693
771
|
}
|
|
694
772
|
/**
|
|
@@ -105,6 +105,9 @@ export interface RunEnvironmentTargetOptions {
|
|
|
105
105
|
export declare const MAX_GRACE_WAIT_MS = 10000;
|
|
106
106
|
export declare const SEAL_POLL_MS = 250;
|
|
107
107
|
export declare const SEAL_WAIT_MS = 30000;
|
|
108
|
+
/** How long past the world's `expiresAt` the target is given before it is stopped: room for a
|
|
109
|
+
* last call in flight at the deadline and for clock skew between the worker and Hue. */
|
|
110
|
+
export declare const DEADLINE_MARGIN_MS = 5000;
|
|
108
111
|
/** The seal wait's bounds. Only tests shorten them; the helpers always use the defaults. */
|
|
109
112
|
export interface SealTiming {
|
|
110
113
|
maxGraceWaitMs: number;
|
|
@@ -123,16 +126,20 @@ export declare function caseTraceparent(span: {
|
|
|
123
126
|
};
|
|
124
127
|
};
|
|
125
128
|
}): string;
|
|
126
|
-
/** What a deployment's credential-free gateway
|
|
127
|
-
*
|
|
128
|
-
* `x-hue-diagnostic`), or `unknown` for anything else. */
|
|
129
|
+
/** What a deployment's credential-free gateway probe said: `on` (the gateway answered, with its
|
|
130
|
+
* health or with a refusal carrying `x-hue-diagnostic`), `off` (the empty 404 the disabled
|
|
131
|
+
* handler answers, with no `x-hue-diagnostic`), or `unknown` for anything else. */
|
|
129
132
|
export type GatewayState = "on" | "off" | "unknown";
|
|
130
133
|
/**
|
|
131
|
-
* Whether the deployment serves the simulation gateway, from
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
134
|
+
* Whether the deployment serves the simulation gateway, from a credential-free probe of the
|
|
135
|
+
* gateway's reserved health path with no app named (`/api/sim/_hue/health`), so the answer does
|
|
136
|
+
* not depend on which apps the deployment mirrors. The gateway answers it on its own host with
|
|
137
|
+
* its health, 200 with `gateway: "simulation"`, and on the application host with a refusal that
|
|
138
|
+
* carries `x-hue-diagnostic` (`unmirrored_provider`: the reserved segment is no app); either is
|
|
139
|
+
* on, since only the enabled gateway writes that header. The disabled handler's empty 404 (no
|
|
140
|
+
* `x-hue-diagnostic`) is off, and anything else (a network failure, a timeout, a redirect, another
|
|
141
|
+
* status or body) is unknown. Probed only after a create was refused; on and off are remembered
|
|
142
|
+
* per origin, unknown is probed again next time.
|
|
136
143
|
*/
|
|
137
144
|
export declare function gatewayState(baseUrl: string, fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>): Promise<GatewayState>;
|
|
138
145
|
/**
|