@hue-run/sdk 0.15.2 → 0.17.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 +30 -21
- package/EVALUATIONS.md +1 -1
- package/README.md +71 -49
- package/dist/blobs.d.ts +35 -17
- package/dist/blobs.js +141 -75
- package/dist/cli/env-file.d.ts +2 -0
- package/dist/cli/env-file.js +2 -0
- package/dist/cli/eval-direct.d.ts +3 -2
- package/dist/cli/eval-direct.js +49 -6
- package/dist/cli/eval.js +109 -22
- package/dist/cli/login.js +2 -3
- package/dist/client.d.ts +3 -1
- package/dist/client.js +70 -20
- package/dist/evals/attempt.d.ts +1 -1
- package/dist/evals/attempt.js +1 -1
- package/dist/evals/checkpoint.d.ts +6 -2
- package/dist/evals/checkpoint.js +75 -12
- package/dist/evals/files.js +12 -0
- package/dist/evals/runner.d.ts +9 -0
- package/dist/evals/runner.js +63 -10
- package/dist/evals/scorer-publication.js +1 -1
- package/dist/evals/simulation.js +4 -2
- package/dist/inline-files.d.ts +38 -29
- package/dist/inline-files.js +288 -83
- package/dist/privacy.d.ts +1 -1
- package/dist/privacy.js +4 -10
- package/dist/safety.d.ts +1 -0
- package/dist/safety.js +7 -1
- package/dist/snapshot.d.ts +6 -3
- package/dist/snapshot.js +70 -25
- package/dist/transport.d.ts +11 -0
- package/dist/transport.js +296 -20
- package/dist/types.d.ts +7 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -10
package/dist/evals/runner.js
CHANGED
|
@@ -213,6 +213,10 @@ async function allPages(page, maximum = 5000) {
|
|
|
213
213
|
* world that cannot be sealed), telemetry not accepted by the one transport the cases share, or
|
|
214
214
|
* a target outcome left uncertain because its world could not be sealed or read back. Starting
|
|
215
215
|
* more cases into it only piles up the same failure; the run is resumed later instead. */
|
|
216
|
+
/** A new attempt replacing an interrupted one, which Hue still holds as started. */
|
|
217
|
+
function retry(previousExecutionId) {
|
|
218
|
+
return previousExecutionId ? { previousExecutionId, allowUncertainRetry: true } : {};
|
|
219
|
+
}
|
|
216
220
|
function systemic(error) {
|
|
217
221
|
// A saved outcome still waiting for its trace (`TraceExportUnacknowledgedError`) is not
|
|
218
222
|
// systemic: the flush failed on an earlier attempt, telemetry may have recovered since, and
|
|
@@ -230,6 +234,7 @@ async function pool(items, concurrency, execute) {
|
|
|
230
234
|
let position = 0;
|
|
231
235
|
const failures = [];
|
|
232
236
|
let stop = false;
|
|
237
|
+
let cancelled;
|
|
233
238
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
234
239
|
while (position < items.length && !stop) {
|
|
235
240
|
const item = items[position++];
|
|
@@ -237,12 +242,20 @@ async function pool(items, concurrency, execute) {
|
|
|
237
242
|
await execute(item);
|
|
238
243
|
}
|
|
239
244
|
catch (error) {
|
|
245
|
+
// The caller's stop: start nothing more, and report the stop once the cases in flight settle.
|
|
246
|
+
if (error instanceof TargetCancelledError) {
|
|
247
|
+
cancelled = error;
|
|
248
|
+
stop = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
240
251
|
failures.push(error);
|
|
241
252
|
if (systemic(error))
|
|
242
253
|
stop = true;
|
|
243
254
|
}
|
|
244
255
|
}
|
|
245
256
|
}));
|
|
257
|
+
if (cancelled)
|
|
258
|
+
throw cancelled;
|
|
246
259
|
if (failures.length === 1)
|
|
247
260
|
throw failures[0];
|
|
248
261
|
if (failures.length) {
|
|
@@ -498,17 +511,19 @@ export async function runExperiment(options) {
|
|
|
498
511
|
scores,
|
|
499
512
|
exportState: "pending",
|
|
500
513
|
...(saved.trace ? { trace: saved.trace } : {}),
|
|
514
|
+
...(saved.attempt ? { attempt: saved.attempt } : {}),
|
|
501
515
|
};
|
|
502
516
|
await store.write(file, prepared);
|
|
503
517
|
return prepared;
|
|
504
518
|
}
|
|
505
519
|
// A case pinned to a world keeps its files apart and removes them when it ends: the agent's
|
|
506
520
|
// copies of its inputs, its work, the evaluator's downloads and the staged outputs.
|
|
507
|
-
|
|
521
|
+
// A retry gets its own, so an agent a dead run left behind cannot write into it.
|
|
522
|
+
const worldDirectoryOf = (itemId, attempt) => join(filesRoot, `world-case-${uuid(itemId)}${attempt ? `-${uuid(attempt)}` : ""}`);
|
|
508
523
|
const runCase = async (item, files) => {
|
|
509
524
|
const file = `case-${uuid(item.id)}`;
|
|
510
|
-
const
|
|
511
|
-
const caseDirectoryOf = (frozen) => frozen.environmentVersionId ?
|
|
525
|
+
const directDirectory = join(filesRoot, `case-${uuid(item.id)}`);
|
|
526
|
+
const caseDirectoryOf = (frozen) => frozen.environmentVersionId ? files.directory : directDirectory;
|
|
512
527
|
let checkpoint;
|
|
513
528
|
try {
|
|
514
529
|
checkpoint = await store.read(file);
|
|
@@ -521,6 +536,24 @@ export async function runExperiment(options) {
|
|
|
521
536
|
throw error;
|
|
522
537
|
}
|
|
523
538
|
files.keepOutputs = checkpoint?.stage === "uploading";
|
|
539
|
+
if (checkpoint && "attempt" in checkpoint)
|
|
540
|
+
files.directory = worldDirectoryOf(item.id, checkpoint.attempt);
|
|
541
|
+
let retryOf;
|
|
542
|
+
// A case left running by a process that died here is as interrupted as one the caller
|
|
543
|
+
// stopped; a fresh world isolates it from anything the dead run's agent still does.
|
|
544
|
+
if (checkpoint?.stage === "running" && store.reclaimed && options.rerunCrashedCases) {
|
|
545
|
+
checkpoint = { ...checkpoint, stage: "interrupted" };
|
|
546
|
+
await store.write(file, checkpoint);
|
|
547
|
+
}
|
|
548
|
+
if (checkpoint?.stage === "interrupted") {
|
|
549
|
+
retryOf = checkpoint.executionId;
|
|
550
|
+
checkpoint = undefined;
|
|
551
|
+
// A direct case's directory goes too, so the retry finds no input copy or output of the
|
|
552
|
+
// stopped attempt.
|
|
553
|
+
await removeCaseFiles(files.directory);
|
|
554
|
+
await removeCaseFiles(directDirectory);
|
|
555
|
+
files.directory = worldDirectoryOf(item.id, retryOf);
|
|
556
|
+
}
|
|
524
557
|
if (checkpoint && checkpoint.stage !== "prepared" && checkpoint.stage !== "uploading") {
|
|
525
558
|
if (checkpoint.stage === "serialization_failed")
|
|
526
559
|
throw new OutcomeSerializationError(checkpoint.executionId);
|
|
@@ -528,6 +561,7 @@ export async function runExperiment(options) {
|
|
|
528
561
|
? await options.client.startExecution(experiment.id, item.id, {
|
|
529
562
|
idempotencyKey: checkpoint.startKey,
|
|
530
563
|
traceExternalId: checkpoint.traceExternalId,
|
|
564
|
+
...retry(checkpoint.previousExecutionId),
|
|
531
565
|
})
|
|
532
566
|
: await options.client.getExecution(checkpoint.executionId);
|
|
533
567
|
throw new UncertainExecutionError(item.id, execution.id);
|
|
@@ -542,7 +576,9 @@ export async function runExperiment(options) {
|
|
|
542
576
|
files.keepOutputs = false;
|
|
543
577
|
}
|
|
544
578
|
if (!checkpoint) {
|
|
545
|
-
if (
|
|
579
|
+
if (options.signal?.aborted)
|
|
580
|
+
throw new TargetCancelledError();
|
|
581
|
+
if (item.execution && item.execution.id !== retryOf)
|
|
546
582
|
throw new UncertainExecutionError(item.id, item.execution.id);
|
|
547
583
|
const frozenCase = await options.client.getExperimentCase(experiment.id, item.id);
|
|
548
584
|
if (frozenCase.datasetVersionId !== version.id)
|
|
@@ -574,13 +610,16 @@ export async function runExperiment(options) {
|
|
|
574
610
|
stage: "starting",
|
|
575
611
|
startKey: randomUUID(),
|
|
576
612
|
traceExternalId: span.traceId,
|
|
613
|
+
...(retryOf ? { previousExecutionId: retryOf } : {}),
|
|
577
614
|
};
|
|
578
615
|
await store.write(file, start);
|
|
579
616
|
const execution = await options.client.startExecution(experiment.id, item.id, {
|
|
580
617
|
idempotencyKey: start.startKey,
|
|
581
618
|
traceExternalId: span.traceId,
|
|
619
|
+
...retry(retryOf),
|
|
582
620
|
});
|
|
583
|
-
|
|
621
|
+
const attempt = retryOf ? { attempt: retryOf } : {};
|
|
622
|
+
await store.write(file, { stage: "running", executionId: execution.id, ...attempt });
|
|
584
623
|
let state = "succeeded";
|
|
585
624
|
let output;
|
|
586
625
|
let generated;
|
|
@@ -604,6 +643,17 @@ export async function runExperiment(options) {
|
|
|
604
643
|
catch (error) {
|
|
605
644
|
if (error instanceof TargetOutcomeUncertainError)
|
|
606
645
|
throw error;
|
|
646
|
+
// A stop the caller asked for is no outcome: the execution stays open for the
|
|
647
|
+
// resumed run to replace instead of being scored as cancelled.
|
|
648
|
+
if (error instanceof TargetCancelledError && options.signal?.aborted) {
|
|
649
|
+
options.hue.recordError(span.span, error);
|
|
650
|
+
await store.write(file, {
|
|
651
|
+
stage: "interrupted",
|
|
652
|
+
executionId: execution.id,
|
|
653
|
+
...attempt,
|
|
654
|
+
});
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
607
657
|
state = error instanceof TargetCancelledError ? "cancelled" : "error";
|
|
608
658
|
targetError = error;
|
|
609
659
|
options.hue.recordError(span.span, error);
|
|
@@ -659,6 +709,7 @@ export async function runExperiment(options) {
|
|
|
659
709
|
...(options.persistResultContent && output !== undefined ? { output } : {}),
|
|
660
710
|
...(state === "error" ? { error: errorPayload(targetError) } : {}),
|
|
661
711
|
files: staged,
|
|
712
|
+
...attempt,
|
|
662
713
|
};
|
|
663
714
|
// Without persisted result content a restart cannot reconstruct the outcome; the
|
|
664
715
|
// saved stage then reports the execution as uncertain instead of guessing.
|
|
@@ -838,21 +889,23 @@ export async function runExperiment(options) {
|
|
|
838
889
|
// A world case's directory goes however the case ends, a forced exit included. An
|
|
839
890
|
// interrupted upload keeps only the staged outputs it resumes from; inputs are downloaded
|
|
840
891
|
// anew for scoring. A direct case has no such directory, so nothing is removed for it.
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
const untrack = onForcedExit(() => releaseWorldFilesSync(worldDirectory, files.keepOutputs));
|
|
892
|
+
const files = { keepOutputs: true, directory: worldDirectoryOf(item.id) };
|
|
893
|
+
const untrack = onForcedExit(() => releaseWorldFilesSync(files.directory, files.keepOutputs));
|
|
844
894
|
try {
|
|
845
895
|
await runCase(item, files);
|
|
846
|
-
await releaseWorldFiles(
|
|
896
|
+
await releaseWorldFiles(files.directory, false);
|
|
847
897
|
}
|
|
848
898
|
catch (error) {
|
|
849
|
-
await releaseWorldFiles(
|
|
899
|
+
await releaseWorldFiles(files.directory, files.keepOutputs);
|
|
850
900
|
throw error;
|
|
851
901
|
}
|
|
852
902
|
finally {
|
|
853
903
|
untrack();
|
|
854
904
|
}
|
|
855
905
|
});
|
|
906
|
+
// A stop that came while the last cases saved their outcomes still leaves the run open.
|
|
907
|
+
if (options.signal?.aborted)
|
|
908
|
+
throw new TargetCancelledError();
|
|
856
909
|
let finish = await store.read("finish");
|
|
857
910
|
if (!finish) {
|
|
858
911
|
finish = { key: randomUUID() };
|
package/dist/evals/simulation.js
CHANGED
|
@@ -391,7 +391,7 @@ export async function runSimulation(options) {
|
|
|
391
391
|
attempt.stage !== "completed" &&
|
|
392
392
|
attempt.scenarioDigest !== scenarioDigest &&
|
|
393
393
|
attempt.scenarioDigest !== previousDigest)
|
|
394
|
-
throw new Error("Recover the unfinished simulation before running a changed scenario or agent revision");
|
|
394
|
+
throw new Error("Recover the unfinished simulation before running a changed scenario or agent revision: run the unchanged one again to finish it, or use another checkpoint directory");
|
|
395
395
|
if (!attempt || attempt.stage === "completed") {
|
|
396
396
|
attempt = {
|
|
397
397
|
scenarioDigest,
|
|
@@ -417,12 +417,14 @@ export async function runSimulation(options) {
|
|
|
417
417
|
];
|
|
418
418
|
}
|
|
419
419
|
const experimentId = checkpointSegment(attempt.experimentId, "experiment id");
|
|
420
|
-
const runUrl = new URL(`/
|
|
420
|
+
const runUrl = new URL(`/runs/${experimentId}`, options.client.baseUrl).toString();
|
|
421
421
|
await options.onProgress?.({ type: "run_created", experimentId, runUrl });
|
|
422
422
|
const requested = requestedConfiguration
|
|
423
423
|
? pinRequestedAttemptV2(requestedConfiguration, (await options.client.getExperiment(experimentId)).config)
|
|
424
424
|
: undefined;
|
|
425
425
|
const report = await runExperiment({
|
|
426
|
+
signal: options.signal,
|
|
427
|
+
rerunCrashedCases: true,
|
|
426
428
|
client: options.client,
|
|
427
429
|
hue: options.hue,
|
|
428
430
|
experimentId,
|
package/dist/inline-files.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Text media above this decoded byte limit is offloaded; binary media always uses a digest. */
|
|
2
2
|
export declare const INLINE_FILE_LIMIT: number;
|
|
3
|
-
/** The longest message text, in UTF-
|
|
3
|
+
/** The longest message text, in UTF-16 code units, inspected for inline files. */
|
|
4
4
|
export declare const MAX_INLINE_FILE_TEXT: number;
|
|
5
5
|
/** Message text one record inspects for inline files in all, in UTF-16 code units. */
|
|
6
6
|
export declare const INLINE_FILE_TEXT_PER_RECORD: number;
|
|
@@ -20,38 +20,47 @@ export interface InlineFileBytes {
|
|
|
20
20
|
* `data:` URL whose data does not decode is taken as UTF-8 too.
|
|
21
21
|
*/
|
|
22
22
|
export declare function inlineFileBytes(content: string): InlineFileBytes;
|
|
23
|
-
/**
|
|
24
|
-
export declare function contentKey(part: Record<string, unknown>): "content" | "data" | undefined;
|
|
25
|
-
/**
|
|
26
|
-
* Replaces inline files longer than {@link INLINE_FILE_LIMIT} in a recorded message attribute
|
|
27
|
-
* with their SHA-256 and byte size, so a span that inlines a large file exports the file's
|
|
28
|
-
* identity instead of being rejected for its size. The part keeps its other fields (`type`,
|
|
29
|
-
* `mime_type`, `modality`, `mediaType`, …). Other attributes, shorter messages and values that
|
|
30
|
-
* are not JSON are returned unchanged.
|
|
31
|
-
*/
|
|
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. */
|
|
23
|
+
/** A part and the exact leaf that contains its bytes. Nested provider metadata stays in place. */
|
|
39
24
|
export interface InlineFilePart {
|
|
40
25
|
part: Record<string, unknown>;
|
|
41
|
-
|
|
26
|
+
owner: Record<string, unknown>;
|
|
27
|
+
key: string;
|
|
42
28
|
content: string;
|
|
43
29
|
pointer: string;
|
|
30
|
+
partPointer: string;
|
|
31
|
+
contentType?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
base64?: boolean;
|
|
44
34
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*/
|
|
35
|
+
export declare function isMediaName(value: unknown): value is string;
|
|
36
|
+
/** Whether the receiver can preview these bytes as text. */
|
|
37
|
+
export declare function isTextMediaType(contentType: string): boolean;
|
|
38
|
+
/** Cheap shared prefilter before bounded parsing of span message attributes. */
|
|
39
|
+
export declare function mayInlineFiles(key: string, value: unknown): boolean;
|
|
40
|
+
/** A printable MIME type with optional parameters, accepted by the upload contract. */
|
|
41
|
+
export declare function isMediaContentType(value: unknown): value is string;
|
|
42
|
+
/** Detect the content pointer, MIME, name, and encoding contract of a provider media part. */
|
|
43
|
+
export declare function contentPath(part: Record<string, unknown>, pointer?: string): InlineFilePart | undefined;
|
|
44
|
+
/** Inline media in document order. The caller applies the decoded text threshold. */
|
|
51
45
|
export declare function largeInlineFileParts(value: unknown): InlineFilePart[];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export
|
|
46
|
+
export declare function mediaBytes(file: InlineFilePart): InlineFileBytes;
|
|
47
|
+
export declare function mediaContentType(file: InlineFilePart, decoded: InlineFileBytes): string;
|
|
48
|
+
export interface FileDigest {
|
|
55
49
|
sha256: string;
|
|
56
50
|
size: number;
|
|
57
|
-
}
|
|
51
|
+
}
|
|
52
|
+
/** Bounded payload identity memo, shared by admission and export, cleared at shutdown. */
|
|
53
|
+
export declare class MediaDigestMemo {
|
|
54
|
+
private entries;
|
|
55
|
+
private units;
|
|
56
|
+
clear(): void;
|
|
57
|
+
digest(file: InlineFilePart, decoded?: InlineFileBytes, traceId?: string): FileDigest;
|
|
58
|
+
bytes(bytes: Uint8Array, traceId: string): FileDigest;
|
|
59
|
+
}
|
|
60
|
+
/** Digest placement removes only the leaf and adds identity to the provider part. */
|
|
61
|
+
export declare function digestPart(file: InlineFilePart, digest: FileDigest): void;
|
|
62
|
+
export declare function hashInlineFiles(key: string, value: unknown, count?: {
|
|
63
|
+
files: number;
|
|
64
|
+
}, memo?: MediaDigestMemo, traceId?: string): unknown;
|
|
65
|
+
/** Nested message attributes retain small media and digest larger media without upload state. */
|
|
66
|
+
export declare function hashNestedInlineFiles(key: string, value: unknown): unknown;
|