@hue-run/sdk 0.1.5 → 0.2.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/ENVIRONMENTS.md +182 -0
- package/EVALUATIONS.md +12 -0
- package/README.md +194 -18
- package/dist/ai-sdk.d.ts +9 -1
- package/dist/ai-sdk.js +34 -8
- package/dist/client.d.ts +121 -6
- package/dist/client.js +329 -56
- package/dist/config.d.ts +11 -2
- package/dist/config.js +36 -7
- package/dist/environment/client.d.ts +73 -0
- package/dist/environment/client.js +209 -0
- package/dist/environment/tools.d.ts +30 -0
- package/dist/environment/tools.js +24 -0
- package/dist/environment/types.d.ts +429 -0
- package/dist/environment/types.js +1 -0
- package/dist/environment.d.ts +5 -0
- package/dist/environment.js +2 -0
- package/dist/evals/attempt.d.ts +454 -0
- package/dist/evals/attempt.js +687 -0
- package/dist/evals/client.d.ts +99 -5
- package/dist/evals/client.js +136 -7
- package/dist/evals/environment-evidence.d.ts +6 -0
- package/dist/evals/environment-evidence.js +123 -0
- package/dist/evals/environment-json.d.ts +3 -0
- package/dist/evals/environment-json.js +76 -0
- package/dist/evals/json.d.ts +9 -1
- package/dist/evals/json.js +14 -6
- package/dist/evals/runner.d.ts +61 -2
- package/dist/evals/runner.js +71 -9
- package/dist/evals/scorer-publication.d.ts +2 -0
- package/dist/evals/scorer-publication.js +84 -0
- package/dist/evals/scorers.d.ts +11 -0
- package/dist/evals/scorers.js +56 -5
- package/dist/evals/simulation.d.ts +184 -0
- package/dist/evals/simulation.js +603 -0
- package/dist/evals/types.d.ts +304 -0
- package/dist/evals.d.ts +5 -1
- package/dist/evals.js +3 -1
- package/dist/experimental-telemetry.d.ts +8 -0
- package/dist/experimental-telemetry.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/managed.d.ts +51 -1
- package/dist/managed.js +11 -1
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +16 -1
- package/dist/receipt.d.ts +12 -1
- package/dist/receipt.js +10 -1
- package/dist/safety.d.ts +1 -2
- package/dist/snapshot.js +4 -0
- package/dist/transport.d.ts +41 -9
- package/dist/transport.js +80 -22
- package/dist/types.d.ts +144 -8
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +51 -15
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function validText(value) {
|
|
2
|
+
return value.isWellFormed() && !value.includes("\u0000");
|
|
3
|
+
}
|
|
4
|
+
function record(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function environmentJson(value, maxBytes = 200_000, maxDepth = 32, maxNodes = 20_000) {
|
|
8
|
+
const pending = [{ value, depth: 0 }];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
let nodes = 0;
|
|
11
|
+
while (pending.length) {
|
|
12
|
+
const current = pending.pop();
|
|
13
|
+
if (++nodes > maxNodes || current.depth > maxDepth)
|
|
14
|
+
throw new RangeError("Environment JSON exceeds depth/node limits");
|
|
15
|
+
const item = current.value;
|
|
16
|
+
if (item === null || typeof item === "boolean")
|
|
17
|
+
continue;
|
|
18
|
+
if (typeof item === "number" && Number.isFinite(item))
|
|
19
|
+
continue;
|
|
20
|
+
if (typeof item === "string" && validText(item))
|
|
21
|
+
continue;
|
|
22
|
+
if (!item || typeof item !== "object" || seen.has(item))
|
|
23
|
+
throw new TypeError("Expected finite environment JSON without cycles or invalid Unicode");
|
|
24
|
+
seen.add(item);
|
|
25
|
+
const keys = Object.keys(item);
|
|
26
|
+
if (Array.isArray(item)) {
|
|
27
|
+
if (keys.length !== item.length)
|
|
28
|
+
throw new TypeError("Sparse/extended arrays are not JSON");
|
|
29
|
+
}
|
|
30
|
+
else if (Object.getPrototypeOf(item) !== Object.prototype &&
|
|
31
|
+
Object.getPrototypeOf(item) !== null)
|
|
32
|
+
throw new TypeError("Environment JSON objects must be plain objects");
|
|
33
|
+
if (Object.getOwnPropertySymbols(item).length)
|
|
34
|
+
throw new TypeError("Environment JSON cannot contain symbol properties");
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
if (!validText(key))
|
|
37
|
+
throw new TypeError("Invalid environment JSON key");
|
|
38
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
39
|
+
if (!("value" in descriptor))
|
|
40
|
+
throw new TypeError("Environment JSON cannot contain accessors");
|
|
41
|
+
pending.push({ value: descriptor.value, depth: current.depth + 1 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (Buffer.byteLength(JSON.stringify(value)) > maxBytes)
|
|
45
|
+
throw new RangeError("Environment JSON exceeds byte limit");
|
|
46
|
+
}
|
|
47
|
+
export function validateEvidenceWorld(value) {
|
|
48
|
+
environmentJson(value, 200_000, 35, 200_000);
|
|
49
|
+
if (!record(value) || Object.keys(value).length !== 1 || !record(value.collections))
|
|
50
|
+
throw new TypeError("Expected an environment world");
|
|
51
|
+
const collections = Object.entries(value.collections);
|
|
52
|
+
if (collections.length > 64)
|
|
53
|
+
throw new RangeError("Environment world exceeds 64 collections");
|
|
54
|
+
let entities = 0;
|
|
55
|
+
for (const [name, entries] of collections) {
|
|
56
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(name) || !record(entries))
|
|
57
|
+
throw new TypeError("Invalid environment collection");
|
|
58
|
+
for (const [id, entity] of Object.entries(entries)) {
|
|
59
|
+
if (++entities > 2000)
|
|
60
|
+
throw new RangeError("Environment world exceeds 2000 entities");
|
|
61
|
+
if (!id.length || id.length > 200 || id === "__proto__" || !record(entity))
|
|
62
|
+
throw new TypeError("Invalid environment entity");
|
|
63
|
+
environmentJson(entity);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function validateEvidenceArguments(value) {
|
|
68
|
+
environmentJson(value, 256 * 1024, 33, 256 * 1024);
|
|
69
|
+
if (!record(value))
|
|
70
|
+
throw new TypeError("Expected environment argument object");
|
|
71
|
+
for (const [name, argument] of Object.entries(value)) {
|
|
72
|
+
if (!name.length || name.length > 64)
|
|
73
|
+
throw new TypeError("Invalid environment argument name");
|
|
74
|
+
environmentJson(argument);
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/evals/json.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { JsonValue } from "../types.js";
|
|
2
|
+
export type JsonBounds = {
|
|
3
|
+
bytes: number;
|
|
4
|
+
nodes: number;
|
|
5
|
+
depth: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const valueBounds: JsonBounds;
|
|
8
|
+
export declare function aggregateBounds(bytes: number): JsonBounds;
|
|
2
9
|
/** Reject lossy JSON serialization before creating requests/checkpoints. */
|
|
3
|
-
export declare function json(value: unknown,
|
|
10
|
+
export declare function json(value: unknown, requested?: JsonBounds | number): JsonValue;
|
|
4
11
|
export declare function digest(value: unknown): string;
|
|
12
|
+
/** SHA-256 hex digest of scorer source, as declared in a `local_code` definition. */
|
|
5
13
|
export declare function sourceDigest(source: string | Uint8Array): string;
|
|
6
14
|
export declare function uuid(value: string): string;
|
package/dist/evals/json.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
export const valueBounds = { bytes: 200_000, nodes: 20_000, depth: 32 };
|
|
3
|
+
export function aggregateBounds(bytes) {
|
|
4
|
+
return { bytes, nodes: Math.ceil(bytes / 2), depth: valueBounds.depth + 8 };
|
|
5
|
+
}
|
|
6
|
+
const isText = (item) => item.isWellFormed() && !item.includes("\u0000");
|
|
2
7
|
/** Reject lossy JSON serialization before creating requests/checkpoints. */
|
|
3
|
-
export function json(value,
|
|
8
|
+
export function json(value, requested = valueBounds) {
|
|
9
|
+
const bounds = typeof requested === "number" ? { ...valueBounds, bytes: requested } : requested;
|
|
4
10
|
const ancestors = new Set();
|
|
5
11
|
let nodes = 0;
|
|
6
12
|
const visit = (item, depth) => {
|
|
7
|
-
if (++nodes >
|
|
13
|
+
if (++nodes > bounds.nodes || depth > bounds.depth)
|
|
8
14
|
throw new RangeError("JSON exceeds depth/node limits");
|
|
9
15
|
if (item === null || typeof item === "boolean")
|
|
10
16
|
return item;
|
|
11
17
|
if (typeof item === "number" && Number.isFinite(item))
|
|
12
18
|
return item;
|
|
13
|
-
if (typeof item === "string" &&
|
|
19
|
+
if (typeof item === "string" && isText(item))
|
|
14
20
|
return item;
|
|
15
21
|
if (!item || typeof item !== "object" || ancestors.has(item))
|
|
16
22
|
throw new TypeError("Expected finite JSON without cycles or invalid Unicode");
|
|
@@ -27,7 +33,8 @@ export function json(value, maxBytes = 200_000) {
|
|
|
27
33
|
throw new TypeError("JSON cannot contain symbol properties");
|
|
28
34
|
const result = Object.create(null);
|
|
29
35
|
for (const key of Object.keys(item).sort()) {
|
|
30
|
-
|
|
36
|
+
if (!isText(key))
|
|
37
|
+
throw new TypeError("Expected finite JSON without cycles or invalid Unicode");
|
|
31
38
|
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
32
39
|
if (!("value" in descriptor))
|
|
33
40
|
throw new TypeError("JSON cannot contain accessors");
|
|
@@ -41,15 +48,16 @@ export function json(value, maxBytes = 200_000) {
|
|
|
41
48
|
}
|
|
42
49
|
};
|
|
43
50
|
const result = visit(value, 0);
|
|
44
|
-
if (Buffer.byteLength(JSON.stringify(result)) >
|
|
51
|
+
if (Buffer.byteLength(JSON.stringify(result)) > bounds.bytes)
|
|
45
52
|
throw new RangeError("JSON exceeds byte limit");
|
|
46
53
|
return result;
|
|
47
54
|
}
|
|
48
55
|
export function digest(value) {
|
|
49
56
|
return createHash("sha256")
|
|
50
|
-
.update(JSON.stringify(json(value, 8 * 1024 * 1024)))
|
|
57
|
+
.update(JSON.stringify(json(value, aggregateBounds(8 * 1024 * 1024))))
|
|
51
58
|
.digest("hex");
|
|
52
59
|
}
|
|
60
|
+
/** SHA-256 hex digest of scorer source, as declared in a `local_code` definition. */
|
|
53
61
|
export function sourceDigest(source) {
|
|
54
62
|
return createHash("sha256").update(source).digest("hex");
|
|
55
63
|
}
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -2,47 +2,106 @@ import type { HueClient } from "../client.js";
|
|
|
2
2
|
import type { HueSpan } from "../types.js";
|
|
3
3
|
import { EvaluationClient } from "./client.js";
|
|
4
4
|
import type { ExperimentCase, JsonValue, LocalScorer } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a case has a started attempt without a saved outcome. The runner never reruns the
|
|
7
|
+
* target; inspect the execution and authorize a new attempt explicitly through `startExecution`.
|
|
8
|
+
*/
|
|
5
9
|
export declare class UncertainExecutionError extends Error {
|
|
10
|
+
/** The affected case (experiment item) ID. */
|
|
6
11
|
readonly caseId: string;
|
|
12
|
+
/** The execution without a saved outcome, when known. */
|
|
7
13
|
readonly executionId?: string | undefined;
|
|
8
|
-
constructor(
|
|
14
|
+
constructor(
|
|
15
|
+
/** The affected case (experiment item) ID. */
|
|
16
|
+
caseId: string,
|
|
17
|
+
/** The execution without a saved outcome, when known. */
|
|
18
|
+
executionId?: string | undefined);
|
|
9
19
|
}
|
|
20
|
+
/** Thrown when a completed target's output is not serializable JSON; the target is not invoked again. */
|
|
10
21
|
export declare class OutcomeSerializationError extends Error {
|
|
22
|
+
/** The execution whose output could not be serialized. */
|
|
11
23
|
readonly executionId: string;
|
|
12
|
-
constructor(
|
|
24
|
+
constructor(
|
|
25
|
+
/** The execution whose output could not be serialized. */
|
|
26
|
+
executionId: string);
|
|
27
|
+
}
|
|
28
|
+
/** Thrown when cooperative caller cancellation stops target execution. */
|
|
29
|
+
export declare class TargetCancelledError extends Error {
|
|
30
|
+
constructor();
|
|
31
|
+
}
|
|
32
|
+
/** Thrown when the target or world may have committed but acknowledgement is unavailable. */
|
|
33
|
+
export declare class TargetOutcomeUncertainError extends Error {
|
|
34
|
+
/** Execution whose target outcome must never be replayed automatically. */
|
|
35
|
+
readonly executionId: string;
|
|
36
|
+
constructor(
|
|
37
|
+
/** Execution whose target outcome must never be replayed automatically. */
|
|
38
|
+
executionId: string, options?: ErrorOptions);
|
|
13
39
|
}
|
|
14
40
|
interface RunnerOptions {
|
|
41
|
+
/** Evaluation API client for the same project and origin as `hue`. */
|
|
15
42
|
client: EvaluationClient;
|
|
43
|
+
/** Dedicated directory (mode 0700) for resumable checkpoints; one per experiment or rescore run. */
|
|
16
44
|
checkpointDirectory: string;
|
|
45
|
+
/** Whether outputs, error messages, evidence and explanations are stored in Hue and in checkpoints. Required. */
|
|
17
46
|
persistResultContent: boolean;
|
|
47
|
+
/** Local callbacks bound to `local_code` scorer pins by digest. */
|
|
18
48
|
scorers?: LocalScorer[];
|
|
49
|
+
/** Cases in flight at once, 1–16. Default 1. */
|
|
19
50
|
concurrency?: number;
|
|
51
|
+
/** Deadline for JSON Schema scoring in its worker, 100–60000 ms. Default 2000. */
|
|
20
52
|
schemaTimeoutMillis?: number;
|
|
53
|
+
/** Resolve sealed world evidence for local scoring and historical rescoring. */
|
|
54
|
+
environmentEvidence?: "required";
|
|
21
55
|
}
|
|
56
|
+
/** Options for {@link runExperiment}. */
|
|
22
57
|
export interface RunExperimentOptions extends RunnerOptions {
|
|
58
|
+
/** Hue tracing client; each case runs inside a `hue.experiment.case` span. */
|
|
23
59
|
hue: HueClient;
|
|
60
|
+
/** Experiment to run; its dataset version must be frozen. */
|
|
24
61
|
experimentId: string;
|
|
62
|
+
/** Whether each case waits for acknowledged trace export or explicitly omits evidence. Required. */
|
|
25
63
|
traceEvidence: {
|
|
64
|
+
/** Wait for trace and log acknowledgement after the case span ends. */
|
|
26
65
|
mode: "required";
|
|
27
66
|
} | {
|
|
67
|
+
/** Store the declared trace ID without evidence. */
|
|
28
68
|
mode: "omit";
|
|
69
|
+
/** Why evidence is omitted, up to 4000 characters. */
|
|
29
70
|
reason: string;
|
|
30
71
|
};
|
|
72
|
+
/** Runs the application for one frozen case; return the output, or `undefined` when unavailable. */
|
|
31
73
|
target(inputs: JsonValue, context: {
|
|
32
74
|
config: JsonValue;
|
|
33
75
|
item: ExperimentCase;
|
|
34
76
|
span: HueSpan;
|
|
77
|
+
executionId: string;
|
|
35
78
|
}): JsonValue | undefined | Promise<JsonValue | undefined>;
|
|
36
79
|
}
|
|
80
|
+
/** Options for {@link rescore}. */
|
|
37
81
|
export interface RescoreOptions extends RunnerOptions {
|
|
82
|
+
/** Evaluation run created with `createEvaluationRun` over existing subjects. */
|
|
38
83
|
runId: string;
|
|
39
84
|
}
|
|
85
|
+
/** Outcome of a runner call. */
|
|
40
86
|
export interface RunnerReport {
|
|
87
|
+
/** Evaluation run the results belong to. */
|
|
41
88
|
runId: string;
|
|
89
|
+
/** Subjects created or scored, in completion order. */
|
|
42
90
|
subjectIds: string[];
|
|
91
|
+
/** Result IDs uploaded by this call. */
|
|
43
92
|
resultIds: string[];
|
|
93
|
+
/** `llm_judge` and `manual` pins left pending for hosted or human scoring. */
|
|
44
94
|
deferredScorerVersionIds: string[];
|
|
45
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Runs every case of a frozen experiment through `target` on this machine, completes each
|
|
98
|
+
* execution, scores it with local scorers and uploads the results, checkpointing so an interrupted
|
|
99
|
+
* run resumes without invoking the target twice.
|
|
100
|
+
*
|
|
101
|
+
* @throws UncertainExecutionError when a case has a started attempt without a saved outcome.
|
|
102
|
+
* @throws OutcomeSerializationError when a completed target's output is not serializable.
|
|
103
|
+
* @throws HueApiError for evaluation API failures; the checkpoint keeps prepared payloads for a retry.
|
|
104
|
+
*/
|
|
46
105
|
export declare function runExperiment(options: RunExperimentOptions): Promise<RunnerReport>;
|
|
47
106
|
/** Scores existing immutable subjects; this API has no target callback. */
|
|
48
107
|
export declare function rescore(options: RescoreOptions): Promise<RunnerReport>;
|
package/dist/evals/runner.js
CHANGED
|
@@ -1,28 +1,60 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { ROOT_CONTEXT } from "@opentelemetry/api";
|
|
3
3
|
import { HueExportError } from "../transport.js";
|
|
4
|
+
import { loadEnvironmentEvidence } from "./environment-evidence.js";
|
|
4
5
|
import { CheckpointStore } from "./checkpoint.js";
|
|
5
6
|
import { json, uuid } from "./json.js";
|
|
6
7
|
import { persistedScore, scoreLocally, validateScorerBindings } from "./scorers.js";
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a case has a started attempt without a saved outcome. The runner never reruns the
|
|
10
|
+
* target; inspect the execution and authorize a new attempt explicitly through `startExecution`.
|
|
11
|
+
*/
|
|
7
12
|
export class UncertainExecutionError extends Error {
|
|
8
13
|
caseId;
|
|
9
14
|
executionId;
|
|
10
|
-
constructor(
|
|
15
|
+
constructor(
|
|
16
|
+
/** The affected case (experiment item) ID. */
|
|
17
|
+
caseId,
|
|
18
|
+
/** The execution without a saved outcome, when known. */
|
|
19
|
+
executionId) {
|
|
11
20
|
super("Execution has no saved outcome. The target will not run again. Inspect it and explicitly authorize a new attempt through startExecution if required.");
|
|
12
21
|
this.caseId = caseId;
|
|
13
22
|
this.executionId = executionId;
|
|
14
23
|
this.name = "UncertainExecutionError";
|
|
15
24
|
}
|
|
16
25
|
}
|
|
26
|
+
/** Thrown when a completed target's output is not serializable JSON; the target is not invoked again. */
|
|
17
27
|
export class OutcomeSerializationError extends Error {
|
|
18
28
|
executionId;
|
|
19
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
/** The execution whose output could not be serialized. */
|
|
31
|
+
executionId) {
|
|
20
32
|
super("Target completed, but its output could not be serialized. Resolve completion explicitly; the runner will not invoke the target again.");
|
|
21
33
|
this.executionId = executionId;
|
|
22
34
|
this.name = "OutcomeSerializationError";
|
|
23
35
|
}
|
|
24
36
|
}
|
|
37
|
+
/** Thrown when cooperative caller cancellation stops target execution. */
|
|
38
|
+
export class TargetCancelledError extends Error {
|
|
39
|
+
constructor() {
|
|
40
|
+
super("Target execution was cancelled");
|
|
41
|
+
this.name = "TargetCancelledError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Thrown when the target or world may have committed but acknowledgement is unavailable. */
|
|
45
|
+
export class TargetOutcomeUncertainError extends Error {
|
|
46
|
+
executionId;
|
|
47
|
+
constructor(
|
|
48
|
+
/** Execution whose target outcome must never be replayed automatically. */
|
|
49
|
+
executionId, options) {
|
|
50
|
+
super(`Target outcome or environment finalization is uncertain for execution ${executionId}; resume will not invoke the target again`, options);
|
|
51
|
+
this.executionId = executionId;
|
|
52
|
+
this.name = "TargetOutcomeUncertainError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
25
55
|
function settings(options) {
|
|
56
|
+
if (options.environmentEvidence !== undefined && options.environmentEvidence !== "required")
|
|
57
|
+
throw new TypeError("environmentEvidence must be required when supplied");
|
|
26
58
|
if (typeof options.persistResultContent !== "boolean")
|
|
27
59
|
throw new TypeError("Choose persistResultContent explicitly: true or false");
|
|
28
60
|
const concurrency = options.concurrency ?? 1;
|
|
@@ -70,13 +102,27 @@ async function pool(items, concurrency, execute) {
|
|
|
70
102
|
if (failures.length)
|
|
71
103
|
throw new AggregateError(failures, "Multiple case operations failed; resume uses saved outcomes");
|
|
72
104
|
}
|
|
73
|
-
async function scoresFor(versions, context, options) {
|
|
105
|
+
async function scoresFor(versions, context, options, executionId) {
|
|
74
106
|
const scores = [];
|
|
107
|
+
let environmentUnavailable = false;
|
|
108
|
+
if (options.environmentEvidence === "required") {
|
|
109
|
+
try {
|
|
110
|
+
context = {
|
|
111
|
+
...context,
|
|
112
|
+
environment: await loadEnvironmentEvidence(options.client, executionId),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
environmentUnavailable = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
75
119
|
for (const version of versions) {
|
|
76
120
|
// Hosted/manual pins remain pending for their authorized executor.
|
|
77
121
|
if (version.definition.kind === "llm_judge" || version.definition.kind === "manual")
|
|
78
122
|
continue;
|
|
79
|
-
const score = persistedScore(
|
|
123
|
+
const score = persistedScore(environmentUnavailable && version.definition.kind === "local_code"
|
|
124
|
+
? { state: "error", error: { type: "EnvironmentEvidenceUnavailable" } }
|
|
125
|
+
: await scoreLocally(version, context, options), options.persistResultContent);
|
|
80
126
|
scores.push({
|
|
81
127
|
key: randomUUID(),
|
|
82
128
|
payload: {
|
|
@@ -104,6 +150,15 @@ async function uploadScores(options, runId, scores, save) {
|
|
|
104
150
|
await save();
|
|
105
151
|
}
|
|
106
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Runs every case of a frozen experiment through `target` on this machine, completes each
|
|
155
|
+
* execution, scores it with local scorers and uploads the results, checkpointing so an interrupted
|
|
156
|
+
* run resumes without invoking the target twice.
|
|
157
|
+
*
|
|
158
|
+
* @throws UncertainExecutionError when a case has a started attempt without a saved outcome.
|
|
159
|
+
* @throws OutcomeSerializationError when a completed target's output is not serializable.
|
|
160
|
+
* @throws HueApiError for evaluation API failures; the checkpoint keeps prepared payloads for a retry.
|
|
161
|
+
*/
|
|
107
162
|
export async function runExperiment(options) {
|
|
108
163
|
const concurrency = settings(options);
|
|
109
164
|
if (!options.traceEvidence || !["required", "omit"].includes(options.traceEvidence.mode))
|
|
@@ -142,6 +197,7 @@ export async function runExperiment(options) {
|
|
|
142
197
|
persistResultContent: options.persistResultContent,
|
|
143
198
|
captureContent: options.hue.captureContent,
|
|
144
199
|
traceEvidence: options.traceEvidence,
|
|
200
|
+
...(options.environmentEvidence ? { environmentEvidence: options.environmentEvidence } : {}),
|
|
145
201
|
});
|
|
146
202
|
const report = {
|
|
147
203
|
runId: experiment.evaluation.id,
|
|
@@ -172,6 +228,8 @@ export async function runExperiment(options) {
|
|
|
172
228
|
const frozenCase = await options.client.getExperimentCase(experiment.id, item.id);
|
|
173
229
|
if (frozenCase.datasetVersionId !== version.id)
|
|
174
230
|
throw new Error("Case is not from the pinned dataset version");
|
|
231
|
+
const targetInputs = json(frozenCase.inputs);
|
|
232
|
+
const targetConfig = json(experiment.config);
|
|
175
233
|
const failureSequenceBefore = options.hue.transport.getFailureSequence();
|
|
176
234
|
checkpoint = await options.hue.withSpan("hue.experiment.case", async (span) => {
|
|
177
235
|
const start = {
|
|
@@ -189,14 +247,17 @@ export async function runExperiment(options) {
|
|
|
189
247
|
let output;
|
|
190
248
|
let targetError;
|
|
191
249
|
try {
|
|
192
|
-
output = await options.target(
|
|
193
|
-
config:
|
|
250
|
+
output = await options.target(targetInputs, {
|
|
251
|
+
config: targetConfig,
|
|
194
252
|
item: structuredClone(frozenCase),
|
|
195
253
|
span,
|
|
254
|
+
executionId: execution.id,
|
|
196
255
|
});
|
|
197
256
|
}
|
|
198
257
|
catch (error) {
|
|
199
|
-
|
|
258
|
+
if (error instanceof TargetOutcomeUncertainError)
|
|
259
|
+
throw error;
|
|
260
|
+
state = error instanceof TargetCancelledError ? "cancelled" : "error";
|
|
200
261
|
targetError = error;
|
|
201
262
|
options.hue.recordError(span.span, error);
|
|
202
263
|
}
|
|
@@ -223,7 +284,7 @@ export async function runExperiment(options) {
|
|
|
223
284
|
hasOutput: output !== undefined,
|
|
224
285
|
...(output !== undefined ? { output } : {}),
|
|
225
286
|
executionState: state,
|
|
226
|
-
}, options);
|
|
287
|
+
}, options, execution.id);
|
|
227
288
|
const complete = {
|
|
228
289
|
idempotencyKey: randomUUID(),
|
|
229
290
|
state,
|
|
@@ -327,6 +388,7 @@ export async function rescore(options) {
|
|
|
327
388
|
.map(({ id, contentDigest }) => ({ id, contentDigest }))
|
|
328
389
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
329
390
|
persistResultContent: options.persistResultContent,
|
|
391
|
+
...(options.environmentEvidence ? { environmentEvidence: options.environmentEvidence } : {}),
|
|
330
392
|
});
|
|
331
393
|
const report = {
|
|
332
394
|
runId: run.id,
|
|
@@ -350,7 +412,7 @@ export async function rescore(options) {
|
|
|
350
412
|
...(subject.hasExpected ? { expected: subject.expected } : {}),
|
|
351
413
|
metadata: subject.metadata,
|
|
352
414
|
executionState: subject.executionState,
|
|
353
|
-
}, options);
|
|
415
|
+
}, options, subject.executionId);
|
|
354
416
|
for (const score of scores)
|
|
355
417
|
score.payload.evaluationItemId = item.id;
|
|
356
418
|
saved = { scores };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { json } from "./json.js";
|
|
3
|
+
const metricName = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.max(64)
|
|
7
|
+
.regex(/^[a-zA-Z][a-zA-Z0-9_]*$/);
|
|
8
|
+
const metric = z.discriminatedUnion("type", [
|
|
9
|
+
z.strictObject({ name: metricName, type: z.literal("boolean") }),
|
|
10
|
+
z.strictObject({ name: metricName, type: z.literal("text") }),
|
|
11
|
+
z.strictObject({
|
|
12
|
+
name: metricName,
|
|
13
|
+
type: z.literal("number"),
|
|
14
|
+
min: z.number().finite().optional(),
|
|
15
|
+
max: z.number().finite().optional(),
|
|
16
|
+
}),
|
|
17
|
+
z.strictObject({
|
|
18
|
+
name: metricName,
|
|
19
|
+
type: z.literal("category"),
|
|
20
|
+
categories: z.array(z.string()).min(1),
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
23
|
+
const metrics = z.array(metric).min(1);
|
|
24
|
+
const jsonValue = z.custom((value) => {
|
|
25
|
+
try {
|
|
26
|
+
json(value);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const judgeConfig = z.strictObject({
|
|
34
|
+
model: z.string(),
|
|
35
|
+
provider: z.string(),
|
|
36
|
+
rubric: z.string().trim(),
|
|
37
|
+
bindings: z.array(z.strictObject({
|
|
38
|
+
name: z.string(),
|
|
39
|
+
path: z.string(),
|
|
40
|
+
required: z.boolean().default(true),
|
|
41
|
+
})),
|
|
42
|
+
maxOutputTokens: z.number().default(1024),
|
|
43
|
+
timeoutMs: z.number().default(60_000),
|
|
44
|
+
temperature: z.number().optional(),
|
|
45
|
+
});
|
|
46
|
+
/** Closed client-side publication contract. The server remains authoritative for
|
|
47
|
+
* full validation; this parser only applies defaults that affect immutable content
|
|
48
|
+
* identity and rejects server-only scorer kinds before any write. A differential
|
|
49
|
+
* test against the server schema guards this deliberately duplicated boundary.
|
|
50
|
+
*/
|
|
51
|
+
const sdkScorerPublication = z.union([
|
|
52
|
+
z.strictObject({
|
|
53
|
+
kind: z.literal("builtin"),
|
|
54
|
+
entry: z.literal("hue.exact_match.v1"),
|
|
55
|
+
config: z.strictObject({}).default({}),
|
|
56
|
+
}),
|
|
57
|
+
z.strictObject({
|
|
58
|
+
kind: z.literal("builtin"),
|
|
59
|
+
entry: z.literal("hue.includes.v1"),
|
|
60
|
+
config: z
|
|
61
|
+
.strictObject({ caseSensitive: z.boolean().default(true) })
|
|
62
|
+
.default({ caseSensitive: true }),
|
|
63
|
+
}),
|
|
64
|
+
z.strictObject({
|
|
65
|
+
kind: z.literal("builtin"),
|
|
66
|
+
entry: z.literal("hue.json_schema.v1"),
|
|
67
|
+
config: z.strictObject({ schema: jsonValue }),
|
|
68
|
+
}),
|
|
69
|
+
z.strictObject({
|
|
70
|
+
kind: z.literal("local_code"),
|
|
71
|
+
language: z.enum(["typescript", "python"]),
|
|
72
|
+
entrypoint: z.string(),
|
|
73
|
+
sourceDigest: z.string(),
|
|
74
|
+
metrics,
|
|
75
|
+
}),
|
|
76
|
+
z.strictObject({ kind: z.literal("manual"), metrics }),
|
|
77
|
+
z.strictObject({ kind: z.literal("llm_judge"), config: judgeConfig, metrics }),
|
|
78
|
+
]);
|
|
79
|
+
export function normalizeScorerDefinitionForPublication(definition) {
|
|
80
|
+
const normalized = sdkScorerPublication.safeParse(definition);
|
|
81
|
+
if (!normalized.success)
|
|
82
|
+
throw new TypeError("Unsupported or invalid SDK scorer definition");
|
|
83
|
+
return json(normalized.data);
|
|
84
|
+
}
|
package/dist/evals/scorers.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { JsonValue, LocalScorer, MetricDefinition, Score, ScoreContext, ScorerDefinition, ScorerVersion } from "./types.js";
|
|
2
|
+
/** Definitions for Hue's built-in scorers, ready to publish with `publishScorerVersion`. */
|
|
2
3
|
export declare const builtins: {
|
|
4
|
+
/** Exact typed JSON equality with the reference output; key order is ignored. */
|
|
3
5
|
exactMatch: () => ScorerDefinition;
|
|
6
|
+
/** The string reference output is contained in the string output. */
|
|
4
7
|
includes: (caseSensitive?: boolean) => ScorerDefinition;
|
|
8
|
+
/** The output satisfies a JSON Schema (draft 2020-12), validated with the optional `ajv` peer in a worker. */
|
|
5
9
|
jsonSchema: (schema: JsonValue) => ScorerDefinition;
|
|
6
10
|
};
|
|
7
11
|
/** The source hash is a caller declaration; closures and installed dependencies are not attested. */
|
|
@@ -13,6 +17,13 @@ export declare function defineLocalScorer(options: {
|
|
|
13
17
|
}): LocalScorer;
|
|
14
18
|
/** Check local callbacks before target invocation; never execute downloaded source code. */
|
|
15
19
|
export declare function validateScorerBindings(versions: ScorerVersion[], scorers?: LocalScorer[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Scores one subject with a pinned scorer version on this machine: built-ins run here, `local_code`
|
|
22
|
+
* pins run the matching callback from `options.scorers`, `manual` pins are skipped. Failures are
|
|
23
|
+
* returned as sanitized error scores.
|
|
24
|
+
*
|
|
25
|
+
* @throws TypeError for an `llm_judge` pin, which must be dispatched through `createJudgeJobs`.
|
|
26
|
+
*/
|
|
16
27
|
export declare function scoreLocally(version: ScorerVersion, context: ScoreContext, options?: {
|
|
17
28
|
scorers?: LocalScorer[];
|
|
18
29
|
schemaTimeoutMillis?: number;
|
package/dist/evals/scorers.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { Worker } from "node:worker_threads";
|
|
3
|
-
import { digest, json, sourceDigest } from "./json.js";
|
|
4
|
+
import { aggregateBounds, digest, json, sourceDigest } from "./json.js";
|
|
5
|
+
import { environmentIncompleteReason, validateEnvironmentEvidence, } from "./environment-evidence.js";
|
|
6
|
+
/** Definitions for Hue's built-in scorers, ready to publish with `publishScorerVersion`. */
|
|
4
7
|
export const builtins = {
|
|
8
|
+
/** Exact typed JSON equality with the reference output; key order is ignored. */
|
|
5
9
|
exactMatch: () => ({
|
|
6
10
|
kind: "builtin",
|
|
7
11
|
entry: "hue.exact_match.v1",
|
|
8
12
|
config: {},
|
|
9
13
|
}),
|
|
14
|
+
/** The string reference output is contained in the string output. */
|
|
10
15
|
includes: (caseSensitive = true) => ({
|
|
11
16
|
kind: "builtin",
|
|
12
17
|
entry: "hue.includes.v1",
|
|
13
18
|
config: { caseSensitive },
|
|
14
19
|
}),
|
|
20
|
+
/** The output satisfies a JSON Schema (draft 2020-12), validated with the optional `ajv` peer in a worker. */
|
|
15
21
|
jsonSchema: (schema) => ({
|
|
16
22
|
kind: "builtin",
|
|
17
23
|
entry: "hue.json_schema.v1",
|
|
@@ -31,7 +37,25 @@ export function defineLocalScorer(options) {
|
|
|
31
37
|
score: options.score,
|
|
32
38
|
};
|
|
33
39
|
}
|
|
40
|
+
/** ajv is an optional peer dependency: only JSON Schema scoring loads it, inside a worker. */
|
|
41
|
+
function schemaValidatorAvailable() {
|
|
42
|
+
try {
|
|
43
|
+
createRequire(import.meta.url).resolve("ajv/dist/2020.js");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
34
50
|
function schemaScore(schema, output, timeoutMillis) {
|
|
51
|
+
if (!schemaValidatorAvailable())
|
|
52
|
+
return Promise.resolve({
|
|
53
|
+
state: "error",
|
|
54
|
+
error: {
|
|
55
|
+
type: "SchemaValidatorUnavailable",
|
|
56
|
+
message: "JSON Schema scoring requires the optional ajv peer dependency: npm install ajv",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
35
59
|
return new Promise((resolve) => {
|
|
36
60
|
const compiled = new URL("./schema-worker.js", import.meta.url);
|
|
37
61
|
const file = existsSync(compiled) ? compiled : new URL("./schema-worker.ts", import.meta.url);
|
|
@@ -75,6 +99,13 @@ export function validateScorerBindings(versions, scorers = []) {
|
|
|
75
99
|
throw new Error("A pinned local scorer has no matching language/source/entrypoint/metric binding");
|
|
76
100
|
}
|
|
77
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Scores one subject with a pinned scorer version on this machine: built-ins run here, `local_code`
|
|
104
|
+
* pins run the matching callback from `options.scorers`, `manual` pins are skipped. Failures are
|
|
105
|
+
* returned as sanitized error scores.
|
|
106
|
+
*
|
|
107
|
+
* @throws TypeError for an `llm_judge` pin, which must be dispatched through `createJudgeJobs`.
|
|
108
|
+
*/
|
|
78
109
|
export async function scoreLocally(version, context, options = {}) {
|
|
79
110
|
const definition = version.definition;
|
|
80
111
|
if (definition.kind === "llm_judge")
|
|
@@ -82,13 +113,25 @@ export async function scoreLocally(version, context, options = {}) {
|
|
|
82
113
|
const timeout = options.schemaTimeoutMillis ?? 2000;
|
|
83
114
|
if (!Number.isInteger(timeout) || timeout < 100 || timeout > 60_000)
|
|
84
115
|
throw new RangeError("schemaTimeoutMillis must be 100–60000");
|
|
85
|
-
if (
|
|
116
|
+
if (context.environment?.validity === "environment_incomplete") {
|
|
117
|
+
try {
|
|
118
|
+
validateEnvironmentEvidence(context.environment);
|
|
119
|
+
return skip(environmentIncompleteReason);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { state: "error", error: { type: "LocalScorerError" } };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!context.hasOutput && !(definition.kind === "local_code" && context.environment))
|
|
86
126
|
return skip("Output evidence is unavailable");
|
|
87
|
-
if (context.output === undefined)
|
|
127
|
+
if (context.hasOutput && context.output === undefined)
|
|
88
128
|
throw new TypeError("hasOutput requires a present JSON output");
|
|
89
129
|
try {
|
|
90
130
|
// Clone and validate inputs so a scorer cannot mutate another scorer's evidence.
|
|
91
|
-
|
|
131
|
+
const { environment, ...ordinaryEvidence } = context;
|
|
132
|
+
json(ordinaryEvidence, aggregateBounds(1024 * 1024));
|
|
133
|
+
if (environment !== undefined)
|
|
134
|
+
validateEnvironmentEvidence(environment);
|
|
92
135
|
const owned = structuredClone(context);
|
|
93
136
|
if (definition.kind === "manual")
|
|
94
137
|
return skip("Manual scoring requires a human session");
|
|
@@ -195,9 +238,17 @@ export function persistedScore(score, persistResultContent) {
|
|
|
195
238
|
explanation: "Local scoring completed; result content storage disabled",
|
|
196
239
|
};
|
|
197
240
|
if (score.state === "error")
|
|
198
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
state: "error",
|
|
243
|
+
error: {
|
|
244
|
+
type: score.error.type === "EnvironmentEvidenceUnavailable"
|
|
245
|
+
? "EnvironmentEvidenceUnavailable"
|
|
246
|
+
: "LocalScorerError",
|
|
247
|
+
},
|
|
248
|
+
};
|
|
199
249
|
// Preserve fixed unavailable reasons, never arbitrary caller explanations.
|
|
200
250
|
const safeReasons = [
|
|
251
|
+
environmentIncompleteReason,
|
|
201
252
|
"Output evidence is unavailable",
|
|
202
253
|
"Reference evidence is unavailable",
|
|
203
254
|
"Includes requires string output and reference",
|