@hue-run/sdk 0.17.0 → 0.18.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.
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
3
+ import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
4
4
  import { extname, join, resolve } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { ArtifactSizeError, HueApiError } from "./client.js";
7
+ import { uuid } from "./json.js";
7
8
  /** Roles the target receives. Organization templates stay with grading, as in the managed protocol. */
8
9
  export const targetFileRoles = [
9
10
  "source",
@@ -413,33 +414,260 @@ export async function uploadOutputFiles(client, executionId, files, save, timing
413
414
  if (file.artifactId)
414
415
  continue;
415
416
  const bytes = await verifiedBytes(file.path, file);
416
- const reserved = await client.reserveArtifact({
417
+ file.artifactId = await publishArtifact(client, {
417
418
  idempotencyKey: `hue-sdk:${executionId}:${file.sha256}:${sha256(Buffer.from(file.filename)).slice(0, 16)}`,
418
419
  filename: file.filename,
419
420
  contentType: file.contentType,
420
421
  byteSize: file.byteSize,
421
422
  sha256: file.sha256,
422
- });
423
- let state = reserved.state;
424
- if (state !== "ready") {
425
- // A verification already running holds the uploaded bytes; Hue refuses another upload then.
426
- if (reserved.copyState !== "acknowledged" && state !== "verifying") {
427
- const upload = await client.requestArtifactUpload(reserved.id);
428
- try {
429
- await client.uploadArtifactBytes(upload, bytes, file.contentType);
430
- }
431
- catch {
432
- // A lost staging acknowledgement or an immutable object already present can only be
433
- // settled by verified completion; never rewrite a final object here.
434
- }
423
+ }, bytes, `Generated file ${file.filename}`, timing);
424
+ await save();
425
+ }
426
+ }
427
+ /** Reserve, stage and verify one artifact; its ID once Hue verified it. Replaying the key settles
428
+ * on the same artifact, including one Hue finished verifying after an earlier attempt stopped. */
429
+ async function publishArtifact(client, declared, body, label, timing) {
430
+ const reserved = await client.reserveArtifact(declared);
431
+ let state = reserved.state;
432
+ if (state !== "ready") {
433
+ // A verification already running holds the uploaded bytes; Hue refuses another upload then.
434
+ if (reserved.copyState !== "acknowledged" && state !== "verifying") {
435
+ // A checked file is read again, whole, only now and only if it still has the bytes its
436
+ // check hashed: a file that changed sends nothing.
437
+ const bytes = body instanceof Uint8Array ? body : await checkedBytes(body);
438
+ const upload = await client.requestArtifactUpload(reserved.id);
439
+ try {
440
+ if (body instanceof Uint8Array)
441
+ await client.uploadArtifactBytes(upload, bytes, declared.contentType);
442
+ // Sent from the write's own buffer a slice at a time, never copied whole.
443
+ else
444
+ await client.uploadArtifactStream(upload, slicesOf(bytes), bytes.byteLength, declared.contentType);
445
+ }
446
+ catch {
447
+ // A lost staging acknowledgement or an immutable object already present can only be
448
+ // settled by verified completion; never rewrite a final object here.
435
449
  }
436
- state = await settleArtifact(client, reserved.id, timing);
437
450
  }
438
- if (state !== "ready")
439
- throw new Error(`Generated file ${file.filename} was not verified by Hue (${state})`);
440
- file.artifactId = reserved.id;
441
- await save();
451
+ state = await settleArtifact(client, reserved.id, timing);
452
+ }
453
+ if (state !== "ready")
454
+ throw new Error(`${label} was not verified by Hue (${state})`);
455
+ return reserved.id;
456
+ }
457
+ /** How much of a case file given by path its check reads at once, to hash it. */
458
+ const CASE_FILE_SLICE = 1024 * 1024;
459
+ /** Bytes as a stream of 1 MiB views of them, made as the request reads them. */
460
+ function slicesOf(bytes) {
461
+ let offset = 0;
462
+ return new ReadableStream({
463
+ pull(controller) {
464
+ if (offset >= bytes.byteLength)
465
+ return controller.close();
466
+ const end = Math.min(bytes.byteLength, offset + CASE_FILE_SLICE);
467
+ controller.enqueue(bytes.subarray(offset, end));
468
+ offset = end;
469
+ },
470
+ }, { highWaterMark: 0 });
471
+ }
472
+ /**
473
+ * The bytes of a checked case file, read whole when it is uploaded: case files are uploaded one
474
+ * at a time, into one buffer the write reuses, so no more than one is held in memory. Only bytes
475
+ * with the checked size and SHA-256 are returned, so the bytes sent are the bytes the check
476
+ * hashed; a file that changed after its check, or can no longer be read, is refused with a
477
+ * `TypeError` before anything is sent. The bytes are valid until the next file is read.
478
+ */
479
+ async function checkedBytes(file) {
480
+ const changed = () => new TypeError(`${file.label} changed after it was checked`);
481
+ const unreadable = (error) => new TypeError(`${file.label} could not be read`, { cause: error });
482
+ const handle = await open(file.path, "r").catch((error) => {
483
+ throw unreadable(error);
484
+ });
485
+ try {
486
+ const read = (buffer, offset) => handle
487
+ .read(buffer, offset, buffer.byteLength - offset, null)
488
+ .then(({ bytesRead }) => bytesRead)
489
+ .catch((error) => {
490
+ throw unreadable(error);
491
+ });
492
+ // Never more than its checked size and one byte, the byte that shows it grew.
493
+ if ((file.scratch.buffer?.byteLength ?? 0) < file.byteSize)
494
+ file.scratch.buffer = Buffer.allocUnsafe(file.byteSize);
495
+ const bytes = file.scratch.buffer.subarray(0, file.byteSize);
496
+ for (let offset = 0; offset < bytes.byteLength;) {
497
+ const bytesRead = await read(bytes, offset);
498
+ if (!bytesRead)
499
+ throw changed();
500
+ offset += bytesRead;
501
+ }
502
+ if ((await read(Buffer.alloc(1), 0)) || sha256(bytes) !== file.sha256)
503
+ throw changed();
504
+ return bytes;
505
+ }
506
+ finally {
507
+ await handle.close().catch(() => undefined);
508
+ }
509
+ }
510
+ /** Roles a pinned case input can carry. */
511
+ const caseFileRoles = [
512
+ ...targetFileRoles,
513
+ "org_template",
514
+ "evaluator_reference",
515
+ ];
516
+ /** The most input files one case pins. */
517
+ const MAX_CASE_FILES = 32;
518
+ /** A case file name as the artifact API takes it: well-formed Unicode of 1–255 UTF-16 code units,
519
+ * as Hue counts them, without a control character or a path separator. */
520
+ function caseFileName(name) {
521
+ if (typeof name !== "string" ||
522
+ !name.length ||
523
+ name.length > 255 ||
524
+ !name.isWellFormed() ||
525
+ [...name].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127 || c === "/" || c === "\\"))
526
+ throw new TypeError("A case file needs a well-formed filename of 1–255 UTF-16 code units without path separators or control characters");
527
+ return name;
528
+ }
529
+ /** A case file given by path, checked: a regular file within the size limit, hashed a slice at a
530
+ * time so it is never held whole. Its emptiness is the caller's check, as for bytes. */
531
+ async function checkedFile(path, label, scratch) {
532
+ if (typeof path !== "string" || !path)
533
+ throw new TypeError(`${label} needs bytes or a path`);
534
+ // A path with no file behind it, or one that cannot be read, is refused as any invalid file.
535
+ const unreadable = (error) => new TypeError(`${label} could not be read`, { cause: error });
536
+ const info = await stat(path).catch((error) => {
537
+ throw unreadable(error);
538
+ });
539
+ if (!info.isFile())
540
+ throw new TypeError(`${label} is not a regular file`);
541
+ if (info.size > outputFileLimits.bytes)
542
+ throw new RangeError(`${label} exceeds 25 MiB`);
543
+ const handle = await open(path, "r").catch((error) => {
544
+ throw unreadable(error);
545
+ });
546
+ try {
547
+ const hash = createHash("sha256");
548
+ const slice = Buffer.allocUnsafe(CASE_FILE_SLICE);
549
+ let byteSize = 0;
550
+ for (;;) {
551
+ const { bytesRead } = await handle
552
+ .read(slice, 0, slice.byteLength, null)
553
+ .catch((error) => {
554
+ throw unreadable(error);
555
+ });
556
+ if (!bytesRead)
557
+ break;
558
+ byteSize += bytesRead;
559
+ // A file that grew past the limit since its size was read is refused, not read whole.
560
+ if (byteSize > outputFileLimits.bytes)
561
+ throw new RangeError(`${label} exceeds 25 MiB`);
562
+ hash.update(slice.subarray(0, bytesRead));
563
+ }
564
+ return { path, label, byteSize, sha256: hash.digest("hex"), scratch };
565
+ }
566
+ finally {
567
+ await handle.close().catch(() => undefined);
568
+ }
569
+ }
570
+ /** A stand-in for an artifact a case file is uploaded as, the length of a real ID. */
571
+ const STAND_IN_ID = "00000000-0000-4000-8000-000000000000";
572
+ /**
573
+ * The `files` of a case write as the case API takes them: references kept, and each file given
574
+ * as bytes or a path checked against the artifact policy (role, name, one of
575
+ * `outputContentTypes`, 1 byte to 25 MiB) and uploaded as a verified artifact. Every file is
576
+ * checked, and then `beforeUpload` is given the references with a stand-in ID for each upload,
577
+ * before the first upload. The reservation key names the version, the case's `externalKey`, the
578
+ * role and the bytes, so the same write sent again reuses its artifacts. Each artifact is pinned
579
+ * once with a role, even when a reference names the artifact a file was uploaded as.
580
+ */
581
+ export async function caseFileReferences(client, versionId, input, beforeUpload = () => undefined, timing = artifactSettling) {
582
+ const files = input.files;
583
+ if (!Array.isArray(files))
584
+ throw new TypeError("files must be a list of case files");
585
+ if (files.length > MAX_CASE_FILES)
586
+ throw new RangeError(`A case pins at most ${MAX_CASE_FILES} files`);
587
+ const prepared = [];
588
+ const seen = new Set();
589
+ const scratch = {};
590
+ for (const [index, file] of files.entries()) {
591
+ const label = `Case file ${index + 1}`;
592
+ if (!file || typeof file !== "object")
593
+ throw new TypeError(`${label} is not a file`);
594
+ if (!caseFileRoles.includes(file.role))
595
+ throw new TypeError(`${label} has an unsupported role ${String(file.role)}`);
596
+ let identity;
597
+ if (file.artifactId !== undefined) {
598
+ // A reference keeps the artifact's own name and type; a file field beside it is refused
599
+ // rather than ignored.
600
+ const fields = file;
601
+ if (fields.filename !== undefined ||
602
+ fields.contentType !== undefined ||
603
+ fields.bytes !== undefined ||
604
+ fields.path !== undefined)
605
+ throw new TypeError(`${label} names an artifactId and a file; give one`);
606
+ const reference = { artifactId: uuid(file.artifactId).toLowerCase(), role: file.role };
607
+ prepared.push(reference);
608
+ identity = `artifact:${reference.artifactId}`;
609
+ }
610
+ else {
611
+ const filename = caseFileName(file.filename);
612
+ if (!outputContentTypes.includes(file.contentType))
613
+ throw new TypeError(`${label} has an unsupported content type ${String(file.contentType)}`);
614
+ if (file.bytes !== undefined && file.path !== undefined)
615
+ throw new TypeError(`${label} names bytes and a path; give one`);
616
+ if (file.bytes !== undefined && !(file.bytes instanceof Uint8Array))
617
+ throw new TypeError(`${label} bytes must be a Uint8Array`);
618
+ // Bytes are kept as given; a path is kept as its size and digest, and read again to upload.
619
+ const body = file.bytes ?? (await checkedFile(file.path, label, scratch));
620
+ const byteSize = body instanceof Uint8Array ? body.byteLength : body.byteSize;
621
+ if (!byteSize)
622
+ throw new RangeError(`${label} is empty`);
623
+ if (byteSize > outputFileLimits.bytes)
624
+ throw new RangeError(`${label} exceeds 25 MiB`);
625
+ const digest = body instanceof Uint8Array ? sha256(body) : body.sha256;
626
+ prepared.push({
627
+ role: file.role,
628
+ filename,
629
+ contentType: file.contentType,
630
+ byteSize,
631
+ sha256: digest,
632
+ body,
633
+ });
634
+ identity = `file:${digest}:${file.contentType}:${filename}`;
635
+ }
636
+ const pinned = `${identity}:${file.role}`;
637
+ if (seen.has(pinned))
638
+ throw new TypeError(`${label} is pinned twice with the role ${file.role}`);
639
+ seen.add(pinned);
640
+ }
641
+ beforeUpload(prepared.map((item) => "artifactId" in item ? item : { artifactId: STAND_IN_ID, role: item.role }));
642
+ const caseKey = sha256(Buffer.from(String(input.externalKey))).slice(0, 16);
643
+ const references = [];
644
+ // A reference and an upload that settle on the same artifact are pinned once: Hue refuses an
645
+ // artifact pinned twice with one role.
646
+ const pins = new Set();
647
+ const pin = (reference) => {
648
+ const identity = `${reference.artifactId.toLowerCase()}:${reference.role}`;
649
+ if (pins.has(identity))
650
+ return;
651
+ pins.add(identity);
652
+ references.push(reference);
653
+ };
654
+ for (const item of prepared) {
655
+ if ("artifactId" in item) {
656
+ pin(item);
657
+ continue;
658
+ }
659
+ const artifactId = await publishArtifact(client, {
660
+ // The key names everything the reservation declares, so a corrected name or type reserves
661
+ // a new artifact instead of colliding with the earlier one.
662
+ idempotencyKey: `hue-sdk:case:${versionId}:${caseKey}:${item.role}:${item.sha256}:${sha256(Buffer.from(item.filename)).slice(0, 16)}:${sha256(Buffer.from(item.contentType)).slice(0, 16)}`,
663
+ filename: item.filename,
664
+ contentType: item.contentType,
665
+ byteSize: item.byteSize,
666
+ sha256: item.sha256,
667
+ }, item.body, `Case file ${item.filename}`, timing);
668
+ pin({ artifactId, role: item.role });
442
669
  }
670
+ return references;
443
671
  }
444
672
  /** Project uploaded staged files as the verified local files handed to scorers. */
445
673
  export function localOutputFiles(files) {
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Reading recorded media back. A trace records a large file as a digest part: the provider part
3
+ * with its content field removed and the file's `sha256` and `size` added, its bytes uploaded
4
+ * under the record's `hue.blobs`. `fetch` downloads one file by digest through a short-lived
5
+ * link Hue issues; `resolve` and {@link rehydrateMedia} put the bytes back into the parts of a
6
+ * recorded value, in the field and encoding each part's shape uses.
7
+ */
8
+ /** Why a media read failed. */
9
+ export type MediaFetchErrorReason =
10
+ /** Hue holds no such file for the trace or snapshot: an unknown trace, another project's, or a
11
+ * digest none of its records list (with `spanId`, that span's record). */
12
+ "not_found"
13
+ /** The trace lists the file, but its bytes were never uploaded or have been erased. */
14
+ | "missing"
15
+ /** Hue could not read its evidence store, again after one retry; `retryAfterSeconds` says when
16
+ * to ask again. */
17
+ | "unavailable"
18
+ /** The key cannot read recorded content: use a Read or Read and write key, and a Read and write
19
+ * key for a snapshot's copy. */
20
+ | "forbidden"
21
+ /** The file is larger than `maxBytes`; nothing was downloaded. */
22
+ | "too_large"
23
+ /** The downloaded bytes do not have the requested SHA-256 or the size Hue reported. */
24
+ | "digest_mismatch"
25
+ /** A connection failure, a timeout, an unexpected status or an invalid response. */
26
+ | "failed";
27
+ /** Thrown by {@link MediaClient.fetch}. The message is fixed and never includes response text. */
28
+ export declare class MediaFetchError extends Error {
29
+ /** What failed. */
30
+ readonly reason: MediaFetchErrorReason;
31
+ /** HTTP status of the answer that failed, when there was one. */
32
+ readonly status?: number | undefined;
33
+ /** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
34
+ readonly retryAfterSeconds?: number | undefined;
35
+ constructor(
36
+ /** What failed. */
37
+ reason: MediaFetchErrorReason,
38
+ /** HTTP status of the answer that failed, when there was one. */
39
+ status?: number | undefined,
40
+ /** Seconds Hue asked the caller to wait, on `unavailable`, when it said. */
41
+ retryAfterSeconds?: number | undefined,
42
+ /** `cause`, the error behind a failure; `snapshot`, set when a snapshot's copy was read, so a
43
+ * `forbidden` message names the Read and write key it needs. */
44
+ options?: {
45
+ cause?: unknown;
46
+ snapshot?: boolean;
47
+ });
48
+ }
49
+ /** One recorded file, downloaded and checked against its digest. */
50
+ export interface MediaFile {
51
+ /** The file's bytes; their SHA-256 is `sha256`. */
52
+ bytes: Uint8Array;
53
+ /** Lowercase hex SHA-256 of `bytes`, as requested. */
54
+ sha256: string;
55
+ /** Byte length of `bytes`. */
56
+ size: number;
57
+ /** The media type the record lists for the file; absent for a snapshot's copy, whose type is
58
+ * the part's own. */
59
+ contentType?: string;
60
+ /** The recorded file name as Hue serves it; absent for a snapshot's copy. */
61
+ fileName?: string;
62
+ }
63
+ /** The file {@link MediaClient.fetch} reads: from a trace, or from a snapshot's pinned copy. */
64
+ export type MediaFetchTarget = {
65
+ /** Hue trace ID (UUID) or the 32-hex OpenTelemetry trace ID. */
66
+ traceId: string;
67
+ /** Lowercase hex SHA-256 the part records. */
68
+ sha256: string;
69
+ /** 16-hex OpenTelemetry span ID of the span that recorded the part. Pass it when known: Hue
70
+ * then reads that span's record alone instead of searching the trace. */
71
+ spanId?: string;
72
+ /** Not set when reading a live trace's file. */
73
+ snapshotId?: undefined;
74
+ } | {
75
+ /** Trace snapshot ID (UUID), such as the one a case converted from a trace keeps. Reading
76
+ * its copy needs a Read and write key. */
77
+ snapshotId: string;
78
+ /** Lowercase hex SHA-256 the part records. */
79
+ sha256: string;
80
+ /** Not set when reading a snapshot's pinned copy. */
81
+ traceId?: undefined;
82
+ /** Not set when reading a snapshot's pinned copy. */
83
+ spanId?: undefined;
84
+ };
85
+ /** Options for {@link MediaClient.fetch}. */
86
+ export interface MediaFetchOptions {
87
+ /** Largest file read into memory, in bytes; a larger one fails with `too_large` before any byte
88
+ * is downloaded. Default 64 MiB, at most 1 GB. */
89
+ maxBytes?: number;
90
+ /** Ends the request, the retry wait and the download. */
91
+ signal?: AbortSignal;
92
+ }
93
+ /** Where {@link MediaClient.resolve} reads the files of a recorded value from, and how it writes
94
+ * them. */
95
+ export type MediaResolveOptions = ({
96
+ /** Hue trace ID (UUID) or the 32-hex OpenTelemetry trace ID the value was recorded in. */
97
+ traceId: string;
98
+ /** 16-hex OpenTelemetry span ID of the span that recorded the value. */
99
+ spanId?: string;
100
+ /** Not set when resolving a live trace's files. */
101
+ snapshotId?: undefined;
102
+ } | {
103
+ /** Trace snapshot ID whose pinned copies hold the files; reading them needs a Read and
104
+ * write key. */
105
+ snapshotId: string;
106
+ /** Not set when resolving a snapshot's pinned copies. */
107
+ traceId?: undefined;
108
+ /** Not set when resolving a snapshot's pinned copies. */
109
+ spanId?: undefined;
110
+ }) & MediaWriteOptions & {
111
+ /** The `hue.blobs` entries of the record the value was read from, in any form Hue reads: an
112
+ * array of compact JSON object strings (what the SDKs write), an array of objects, or one
113
+ * string holding the JSON array. A part that declares no media type, read from a copy that
114
+ * carries none (a snapshot's), takes the type of the entry with its SHA-256 for its `data:`
115
+ * URL, rather than `application/octet-stream`. A part whose content field an entry names and
116
+ * that holds only the first 16 KiB of its file is reported as `prefix_only`. Entries that do
117
+ * not parse are ignored. */
118
+ blobs?: readonly (string | Readonly<Record<string, unknown>>)[] | string;
119
+ /** Files downloaded at once, 1–16. Default 4. */
120
+ concurrency?: number;
121
+ /** Largest file read, as in {@link MediaFetchOptions.maxBytes}. */
122
+ maxBytes?: number;
123
+ /** Ends every request and download. */
124
+ signal?: AbortSignal;
125
+ };
126
+ /** How resolved bytes are written into a part. */
127
+ export interface MediaWriteOptions {
128
+ /**
129
+ * `"dataUrl"` (the default) writes text: a `data:` URL where the part's field takes one (OpenAI
130
+ * `image_url` and `file_data`, Responses `input_image`, `input_file` and `computer_screenshot`,
131
+ * AI SDK `image` and `file` `data`), and bare base64 where its convention requires it (GenAI
132
+ * `blob` `content`, OpenAI `input_audio` and assistant `audio` `data`, Anthropic `source.data`,
133
+ * Gemini `inlineData.data`, Vertex `media` `data`, Bedrock `source.bytes`), so the value can be
134
+ * sent to the provider again. `"bytes"` writes the `Uint8Array` itself in the same field.
135
+ */
136
+ as?: "dataUrl" | "bytes";
137
+ }
138
+ /** A part {@link MediaClient.resolve} or {@link rehydrateMedia} left unchanged. */
139
+ export interface MediaResolveFailure {
140
+ /** RFC 6901 JSON pointer of the part in the value. */
141
+ pointer: string;
142
+ /** The SHA-256 the part records, or for `prefix_only` the SHA-256 of the entry naming its
143
+ * content field. */
144
+ sha256: string;
145
+ /** Why its bytes were not written: as {@link MediaFetchError.reason}, `not_found` when no local
146
+ * file has the digest, or `prefix_only` for a part whose content field holds only the first
147
+ * 16 KiB of its file, as a `blobs` entry names it; {@link MediaClient.fetch} returns that file
148
+ * whole. */
149
+ reason: MediaFetchErrorReason | "prefix_only";
150
+ }
151
+ /** A recorded value with the bytes of its digest parts written back. */
152
+ export interface MediaResolution<T = unknown> {
153
+ /** A new value: the input is never changed. Each resolved part holds its bytes in its content
154
+ * field and no longer carries `sha256` and `size`. */
155
+ value: T;
156
+ /** Parts whose bytes were written. */
157
+ resolved: number;
158
+ /** Parts left unchanged, in reading order. */
159
+ failures: MediaResolveFailure[];
160
+ }
161
+ /** A local copy of a file {@link rehydrateMedia} reads by digest: a runner's verified case file
162
+ * (`LocalFile`, read from `path`) or bytes already in memory. */
163
+ export type MediaSourceFile = {
164
+ /** Lowercase hex SHA-256 of the file's bytes. */
165
+ sha256: string;
166
+ /** The file's media type, used when the part declares none. */
167
+ contentType?: string;
168
+ } & ({
169
+ /** Path of the file on this machine. */
170
+ path: string;
171
+ /** Not set when the file is given as a path. */
172
+ bytes?: undefined;
173
+ } | {
174
+ /** The file's bytes. */
175
+ bytes: Uint8Array;
176
+ /** Not set when the file is given as bytes. */
177
+ path?: undefined;
178
+ });
179
+ /** The least time a download may go without a byte before it ends: two minutes. Tests shorten it;
180
+ * it is not exported from the package. */
181
+ export declare const mediaDownload: {
182
+ minIdleMillis: number;
183
+ };
184
+ /**
185
+ * Hue's media reads, `client.media`: one recorded file by digest (`fetch`), and every digest part
186
+ * of a recorded value written back with its bytes (`resolve`). The project key is sent to Hue
187
+ * only; the file itself comes from a short-lived link fetched without it.
188
+ */
189
+ export declare class MediaClient {
190
+ #private;
191
+ /**
192
+ * Downloads one recorded file and checks it against its digest: from a trace (Hue reads the
193
+ * span's record that lists the digest under `hue.blobs`) or from a snapshot's pinned copy. A
194
+ * read Hue answers as unavailable (503 or 429) is asked once more after its `Retry-After`, at
195
+ * most 10 seconds, and so is one that lost its connection or timed out.
196
+ *
197
+ * @throws MediaFetchError for a file Hue does not hold, a refused key, an unavailable store, a
198
+ * file over `maxBytes`, bytes that do not match the digest or a failed request.
199
+ * @throws TypeError for an invalid trace, span or snapshot ID or digest.
200
+ */
201
+ fetch(target: MediaFetchTarget, options?: MediaFetchOptions): Promise<MediaFile>;
202
+ /**
203
+ * Writes the bytes of every digest part in `value` back into its content field, the reverse of
204
+ * the SDK's media capture: each distinct digest is downloaded once with {@link fetch}, at most
205
+ * `concurrency` at a time. Returns a new value; a part whose file cannot be read stays as it is
206
+ * and is reported in `failures`. Parse attributes recorded as JSON text (such as
207
+ * `gen_ai.input.messages`) before passing them; pass the record's `hue.blobs` as `blobs` when
208
+ * reading a snapshot, whose copies carry no media type.
209
+ *
210
+ * @throws TypeError for invalid identifiers or options; never for one file's failure.
211
+ */
212
+ resolve<T>(value: T, options: MediaResolveOptions): Promise<MediaResolution<T>>;
213
+ }
214
+ /**
215
+ * Writes case files back into the digest parts of a case's inputs, by SHA-256: the files a runner
216
+ * hands a target (`context.files`) or any bytes the caller holds, so a target can send the
217
+ * recorded model call again with the real image, document or audio. Each file is read once and
218
+ * checked against its digest; a part with no matching file, or whose file does not match, stays
219
+ * as it is and is reported in `failures`. Returns a new value; the input is never changed.
220
+ */
221
+ export declare function rehydrateMedia<T>(value: T, files: readonly MediaSourceFile[], options?: MediaWriteOptions): Promise<MediaResolution<T>>;