@hue-run/sdk 0.1.2

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.
@@ -0,0 +1,14 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { Ajv2020 } from "ajv/dist/2020.js";
3
+ try {
4
+ // No asynchronous loading, custom formats, coercion, defaults or data mutation.
5
+ const ajv = new Ajv2020({ strict: false, allErrors: false, validateFormats: false });
6
+ const validate = ajv.compile(workerData.schema);
7
+ if ("$async" in validate && validate.$async)
8
+ throw new Error("Async schemas are unsupported");
9
+ parentPort.postMessage({ match: validate(workerData.output) === true });
10
+ }
11
+ catch {
12
+ parentPort.postMessage({ error: true });
13
+ }
14
+ parentPort.close();
@@ -0,0 +1,21 @@
1
+ import type { JsonValue, LocalScorer, MetricDefinition, Score, ScoreContext, ScorerDefinition, ScorerVersion } from "./types.js";
2
+ export declare const builtins: {
3
+ exactMatch: () => ScorerDefinition;
4
+ includes: (caseSensitive?: boolean) => ScorerDefinition;
5
+ jsonSchema: (schema: JsonValue) => ScorerDefinition;
6
+ };
7
+ /** The source hash is a caller declaration; closures and installed dependencies are not attested. */
8
+ export declare function defineLocalScorer(options: {
9
+ source: string | Uint8Array;
10
+ entrypoint: string;
11
+ metrics: MetricDefinition[];
12
+ score: LocalScorer["score"];
13
+ }): LocalScorer;
14
+ /** Check local callbacks before target invocation; never execute downloaded source code. */
15
+ export declare function validateScorerBindings(versions: ScorerVersion[], scorers?: LocalScorer[]): void;
16
+ export declare function scoreLocally(version: ScorerVersion, context: ScoreContext, options?: {
17
+ scorers?: LocalScorer[];
18
+ schemaTimeoutMillis?: number;
19
+ }): Promise<Score>;
20
+ export declare function validateScore(value: Score, definition: ScorerDefinition): Score;
21
+ export declare function persistedScore(score: Score, persistResultContent: boolean): Score;
@@ -0,0 +1,212 @@
1
+ import { existsSync } from "node:fs";
2
+ import { Worker } from "node:worker_threads";
3
+ import { digest, json, sourceDigest } from "./json.js";
4
+ export const builtins = {
5
+ exactMatch: () => ({
6
+ kind: "builtin",
7
+ entry: "hue.exact_match.v1",
8
+ config: {},
9
+ }),
10
+ includes: (caseSensitive = true) => ({
11
+ kind: "builtin",
12
+ entry: "hue.includes.v1",
13
+ config: { caseSensitive },
14
+ }),
15
+ jsonSchema: (schema) => ({
16
+ kind: "builtin",
17
+ entry: "hue.json_schema.v1",
18
+ config: { schema: json(schema) },
19
+ }),
20
+ };
21
+ /** The source hash is a caller declaration; closures and installed dependencies are not attested. */
22
+ export function defineLocalScorer(options) {
23
+ return {
24
+ definition: {
25
+ kind: "local_code",
26
+ language: "typescript",
27
+ entrypoint: options.entrypoint,
28
+ sourceDigest: sourceDigest(options.source),
29
+ metrics: options.metrics,
30
+ },
31
+ score: options.score,
32
+ };
33
+ }
34
+ function schemaScore(schema, output, timeoutMillis) {
35
+ return new Promise((resolve) => {
36
+ const compiled = new URL("./schema-worker.js", import.meta.url);
37
+ const file = existsSync(compiled) ? compiled : new URL("./schema-worker.ts", import.meta.url);
38
+ const worker = new Worker(file, {
39
+ workerData: { schema, output },
40
+ resourceLimits: { maxOldGenerationSizeMb: 64 },
41
+ });
42
+ let settled = false;
43
+ const finish = async (result) => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ clearTimeout(timer);
48
+ await worker.terminate();
49
+ resolve(result);
50
+ };
51
+ const timer = setTimeout(() => void finish({ state: "error", error: { type: "SchemaTimeout" } }), timeoutMillis);
52
+ worker.once("message", (value) => {
53
+ void finish(typeof value.match === "boolean"
54
+ ? match(value.match, "JSON Schema conformance evaluated locally")
55
+ : { state: "error", error: { type: "InvalidSchema" } });
56
+ });
57
+ worker.once("error", () => void finish({ state: "error", error: { type: "SchemaWorkerError" } }));
58
+ worker.once("exit", () => {
59
+ if (!settled)
60
+ void finish({ state: "error", error: { type: "SchemaWorkerExit" } });
61
+ });
62
+ });
63
+ }
64
+ function match(value, explanation) {
65
+ return { state: "scored", metrics: [{ name: "match", value, passed: value }], explanation };
66
+ }
67
+ function skip(explanation) {
68
+ return { state: "skipped", explanation };
69
+ }
70
+ /** Check local callbacks before target invocation; never execute downloaded source code. */
71
+ export function validateScorerBindings(versions, scorers = []) {
72
+ for (const { definition } of versions) {
73
+ if (definition.kind === "local_code" &&
74
+ !scorers.some((local) => digest(local.definition) === digest(definition)))
75
+ throw new Error("A pinned local scorer has no matching language/source/entrypoint/metric binding");
76
+ }
77
+ }
78
+ export async function scoreLocally(version, context, options = {}) {
79
+ const definition = version.definition;
80
+ if (definition.kind === "llm_judge")
81
+ throw new TypeError("Hosted judge pins require dispatch through createJudgeJobs; do not submit a local result");
82
+ const timeout = options.schemaTimeoutMillis ?? 2000;
83
+ if (!Number.isInteger(timeout) || timeout < 100 || timeout > 60_000)
84
+ throw new RangeError("schemaTimeoutMillis must be 100–60000");
85
+ if (!context.hasOutput)
86
+ return skip("Output evidence is unavailable");
87
+ if (context.output === undefined)
88
+ throw new TypeError("hasOutput requires a present JSON output");
89
+ try {
90
+ // Clone and validate inputs so a scorer cannot mutate another scorer's evidence.
91
+ json(context, 1024 * 1024);
92
+ const owned = structuredClone(context);
93
+ if (definition.kind === "manual")
94
+ return skip("Manual scoring requires a human session");
95
+ if (definition.kind === "local_code") {
96
+ const binding = options.scorers?.find((local) => digest(local.definition) === digest(definition));
97
+ if (!binding)
98
+ return { state: "error", error: { type: "ScorerBindingUnavailable" } };
99
+ return validateScore(await binding.score(owned), definition);
100
+ }
101
+ if (definition.entry === "hue.json_schema.v1")
102
+ return await schemaScore(definition.config.schema, owned.output, timeout);
103
+ if (!owned.hasExpected)
104
+ return skip("Reference evidence is unavailable");
105
+ if (definition.entry === "hue.exact_match.v1")
106
+ return match(digest(owned.output) === digest(owned.expected), "JSON exact match evaluated locally");
107
+ if (typeof owned.output !== "string" || typeof owned.expected !== "string")
108
+ return skip("Includes requires string output and reference");
109
+ const normalize = (value) => definition.config.caseSensitive ? value : value.toLowerCase();
110
+ return match(normalize(owned.output).includes(normalize(owned.expected)), "String inclusion evaluated locally");
111
+ }
112
+ catch {
113
+ // Arbitrary thrown messages can include secrets/output. Keep scorer failures typed and sanitized.
114
+ return { state: "error", error: { type: "LocalScorerError" } };
115
+ }
116
+ }
117
+ export function validateScore(value, definition) {
118
+ json(value);
119
+ const score = structuredClone(value);
120
+ const keys = (object, allowed) => {
121
+ if (Object.keys(object).some((key) => !allowed.includes(key)))
122
+ throw new TypeError("Unexpected scorer result field");
123
+ };
124
+ if (score.state === "error") {
125
+ keys(score, ["state", "error"]);
126
+ if (!score.error ||
127
+ typeof score.error.type !== "string" ||
128
+ !score.error.type ||
129
+ score.error.type.length > 200)
130
+ throw new TypeError("Scorer error requires a bounded type");
131
+ keys(score.error, ["type", "message"]);
132
+ if (score.error.message !== undefined &&
133
+ (typeof score.error.message !== "string" || score.error.message.length > 4000))
134
+ throw new TypeError("Invalid scorer error message");
135
+ return score;
136
+ }
137
+ if (score.state === "skipped") {
138
+ keys(score, ["state", "explanation"]);
139
+ if (typeof score.explanation !== "string" ||
140
+ !score.explanation.trim() ||
141
+ score.explanation.length > 4000)
142
+ throw new TypeError("Skipped score requires a bounded reason");
143
+ return score;
144
+ }
145
+ if (score.state !== "scored" || !Array.isArray(score.metrics))
146
+ throw new TypeError("Invalid scorer result");
147
+ keys(score, ["state", "metrics", "explanation", "evidence"]);
148
+ const metrics = definition.kind === "builtin" ? [{ name: "match", type: "boolean" }] : definition.metrics;
149
+ if (score.metrics.length !== metrics.length ||
150
+ new Set(score.metrics.map((m) => m.name)).size !== metrics.length)
151
+ throw new TypeError("Scorer result must contain every declared metric once");
152
+ for (const expected of metrics) {
153
+ const actual = score.metrics.find((item) => item.name === expected.name);
154
+ if (!actual || (actual.passed !== undefined && typeof actual.passed !== "boolean"))
155
+ throw new TypeError("Invalid metric");
156
+ keys(actual, ["name", "value", "passed"]);
157
+ const value = actual.value;
158
+ if (expected.type === "number") {
159
+ if (typeof value !== "number" ||
160
+ !Number.isFinite(value) ||
161
+ (expected.min !== undefined && value < expected.min) ||
162
+ (expected.max !== undefined && value > expected.max))
163
+ throw new TypeError("Invalid numeric metric");
164
+ }
165
+ else if (expected.type === "category") {
166
+ if (typeof value !== "string" || !expected.categories.includes(value))
167
+ throw new TypeError("Invalid category metric");
168
+ }
169
+ else if (typeof value !== (expected.type === "text" ? "string" : "boolean"))
170
+ throw new TypeError("Invalid metric type");
171
+ if (typeof value === "string" && value.length > 4000)
172
+ throw new TypeError("Metric text is too long");
173
+ }
174
+ if (score.explanation !== undefined &&
175
+ (typeof score.explanation !== "string" ||
176
+ !score.explanation.trim() ||
177
+ score.explanation.length > 4000))
178
+ throw new TypeError("Invalid explanation");
179
+ const evidence = score.evidence;
180
+ const hasEvidence = evidence !== undefined &&
181
+ evidence !== null &&
182
+ (typeof evidence !== "string" || evidence.trim().length > 0) &&
183
+ (typeof evidence !== "object" || Object.keys(evidence).length > 0);
184
+ if (!score.explanation && !hasEvidence)
185
+ throw new TypeError("Scored results require an explanation or evidence");
186
+ return score;
187
+ }
188
+ export function persistedScore(score, persistResultContent) {
189
+ if (persistResultContent)
190
+ return score;
191
+ if (score.state === "scored")
192
+ return {
193
+ state: "scored",
194
+ metrics: score.metrics,
195
+ explanation: "Local scoring completed; result content storage disabled",
196
+ };
197
+ if (score.state === "error")
198
+ return { state: "error", error: { type: "LocalScorerError" } };
199
+ // Preserve fixed unavailable reasons, never arbitrary caller explanations.
200
+ const safeReasons = [
201
+ "Output evidence is unavailable",
202
+ "Reference evidence is unavailable",
203
+ "Includes requires string output and reference",
204
+ "Manual scoring requires a human session",
205
+ ];
206
+ return {
207
+ state: "skipped",
208
+ explanation: safeReasons.includes(score.explanation)
209
+ ? score.explanation
210
+ : "Local scoring skipped; result content storage disabled",
211
+ };
212
+ }
@@ -0,0 +1,301 @@
1
+ import type { JsonValue } from "../types.js";
2
+ export type { JsonValue } from "../types.js";
3
+ export interface Page<T> {
4
+ items: T[];
5
+ nextCursor: string | null;
6
+ }
7
+ export interface PageOptions {
8
+ after?: string;
9
+ limit?: number;
10
+ }
11
+ export interface Identity {
12
+ name: string;
13
+ slug: string;
14
+ description?: string;
15
+ }
16
+ export interface Dataset extends Identity {
17
+ id: string;
18
+ versions: DatasetVersion[];
19
+ }
20
+ export interface DatasetVersion {
21
+ id: string;
22
+ datasetId: string;
23
+ version: number;
24
+ revision: number;
25
+ frozenAt: string | null;
26
+ contentDigest: string | null;
27
+ }
28
+ export interface DatasetCase {
29
+ id: string;
30
+ externalKey: string;
31
+ inputs: JsonValue;
32
+ expected?: JsonValue;
33
+ metadata: Record<string, JsonValue>;
34
+ datasetVersionId: string;
35
+ }
36
+ export interface ExperimentCase extends DatasetCase {
37
+ hasExpected: boolean;
38
+ }
39
+ export interface CaseWrite {
40
+ expectedRevision: number;
41
+ externalKey: string;
42
+ inputs: JsonValue;
43
+ expected?: JsonValue;
44
+ metadata?: Record<string, JsonValue>;
45
+ }
46
+ export type MetricDefinition = {
47
+ name: string;
48
+ type: "boolean" | "text";
49
+ } | {
50
+ name: string;
51
+ type: "number";
52
+ min?: number;
53
+ max?: number;
54
+ } | {
55
+ name: string;
56
+ type: "category";
57
+ categories: string[];
58
+ };
59
+ export type ScorerDefinition = {
60
+ kind: "builtin";
61
+ entry: "hue.exact_match.v1";
62
+ config: Record<string, never>;
63
+ } | {
64
+ kind: "builtin";
65
+ entry: "hue.includes.v1";
66
+ config: {
67
+ caseSensitive: boolean;
68
+ };
69
+ } | {
70
+ kind: "builtin";
71
+ entry: "hue.json_schema.v1";
72
+ config: {
73
+ schema: JsonValue;
74
+ };
75
+ } | {
76
+ kind: "local_code";
77
+ language: "typescript" | "python";
78
+ entrypoint: string;
79
+ sourceDigest: string;
80
+ metrics: MetricDefinition[];
81
+ } | {
82
+ kind: "manual";
83
+ metrics: MetricDefinition[];
84
+ } | {
85
+ kind: "llm_judge";
86
+ config: JudgeConfig;
87
+ metrics: MetricDefinition[];
88
+ };
89
+ export interface JudgeConfig {
90
+ model: string;
91
+ provider: string;
92
+ rubric: string;
93
+ bindings: {
94
+ name: string;
95
+ path: string;
96
+ required: boolean;
97
+ }[];
98
+ maxOutputTokens: number;
99
+ timeoutMs: number;
100
+ temperature?: number;
101
+ }
102
+ export interface ResultSummary {
103
+ id: string;
104
+ itemId: string;
105
+ scorerVersionId: string;
106
+ state: "scored" | "error" | "skipped";
107
+ }
108
+ export interface StoredResult extends ResultSummary {
109
+ runId: string;
110
+ metrics: Metric[];
111
+ explanation: string | null;
112
+ evidence: JsonValue;
113
+ error: TypedError | null;
114
+ sourceDigest: string | null;
115
+ }
116
+ export interface JudgeJob {
117
+ id: string;
118
+ runId: string;
119
+ itemId: string;
120
+ scorerVersionId: string;
121
+ state: "queued" | "running" | "completed" | "cancelled" | "uncertain";
122
+ chargeState: "unreserved" | "reserved" | "settled" | "uncertain";
123
+ /** Preserved charge state before any separately verified reconciliation. */
124
+ originalChargeState: JudgeJob["chargeState"];
125
+ reservationMicroUsd: number;
126
+ actualMicroUsd: number | null;
127
+ reconciliation: {
128
+ jobId: string;
129
+ actualMicroUsd: number;
130
+ evidenceReference: string;
131
+ reason: string;
132
+ createdAt: string;
133
+ } | null;
134
+ priceQuote: JsonValue;
135
+ receipt: JsonValue;
136
+ workflowId: string | null;
137
+ cancelRequestedAt: string | null;
138
+ cancellationReason: string | null;
139
+ createdAt: string;
140
+ startedAt: string | null;
141
+ finishedAt: string | null;
142
+ }
143
+ export interface JudgeBudget {
144
+ projectId: string;
145
+ configured: boolean;
146
+ /** Credential resolution does not establish provider acceptance or available funds. */
147
+ authentication?: {
148
+ status: "available" | "unavailable";
149
+ method: "api-key" | "oidc" | null;
150
+ verification: "credential_resolution";
151
+ };
152
+ enabled: boolean;
153
+ allowanceMicroUsd: number;
154
+ reservedMicroUsd: number;
155
+ spentMicroUsd: number;
156
+ maxInFlight: number;
157
+ blocked: boolean;
158
+ }
159
+ export interface Scorer extends Identity {
160
+ id: string;
161
+ versions?: ScorerVersion[];
162
+ }
163
+ export interface ScorerVersion {
164
+ id: string;
165
+ contentDigest: string;
166
+ definition: ScorerDefinition;
167
+ }
168
+ export type TerminalState = "succeeded" | "error" | "cancelled";
169
+ export interface Execution {
170
+ id: string;
171
+ attempt: number;
172
+ state: TerminalState | "started" | "uncertain";
173
+ traceExternalId: string | null;
174
+ subjectId?: string | null;
175
+ }
176
+ export interface ExperimentItem {
177
+ id: string;
178
+ externalKey: string;
179
+ hasExpected: boolean;
180
+ execution: Execution | null;
181
+ }
182
+ export interface EvaluationRun {
183
+ id: string;
184
+ name: string;
185
+ scorerVersions: ScorerVersion[];
186
+ itemCount: number;
187
+ scores: {
188
+ scored: number;
189
+ error: number;
190
+ skipped: number;
191
+ pending: number;
192
+ };
193
+ }
194
+ export interface Experiment {
195
+ id: string;
196
+ name: string;
197
+ datasetVersionId: string;
198
+ config: JsonValue;
199
+ configDigest: string;
200
+ evaluation: EvaluationRun;
201
+ caseCount: number;
202
+ finishedAt: string | null;
203
+ execution: {
204
+ unstarted: number;
205
+ started: number;
206
+ uncertain: number;
207
+ succeeded: number;
208
+ error: number;
209
+ cancelled: number;
210
+ };
211
+ }
212
+ export interface TypedError {
213
+ type: string;
214
+ message?: string;
215
+ }
216
+ export interface StartExecution {
217
+ idempotencyKey: string;
218
+ traceExternalId?: string;
219
+ previousExecutionId?: string;
220
+ allowUncertainRetry?: boolean;
221
+ }
222
+ export interface CompleteExecution {
223
+ idempotencyKey: string;
224
+ state: TerminalState;
225
+ output?: JsonValue;
226
+ error?: TypedError;
227
+ expectedTraceRevision?: number;
228
+ traceEvidence?: "required" | "omit";
229
+ omissionReason?: string;
230
+ }
231
+ export interface Completion {
232
+ executionId: string;
233
+ subjectId: string;
234
+ traceSnapshotId: string | null;
235
+ evaluationItemId: string;
236
+ }
237
+ export interface EvaluationItem {
238
+ id: string;
239
+ subjectId: string;
240
+ hasOutput: boolean;
241
+ traceSnapshotId: string | null;
242
+ }
243
+ export interface Subject {
244
+ id: string;
245
+ inputs: JsonValue;
246
+ hasOutput: boolean;
247
+ output?: JsonValue;
248
+ hasExpected: boolean;
249
+ expected?: JsonValue;
250
+ metadata: Record<string, JsonValue>;
251
+ contentDigest: string;
252
+ outputEvidence: "available" | "unavailable";
253
+ executionState: TerminalState;
254
+ traceSnapshotId: string | null;
255
+ caseId: string;
256
+ datasetVersionId: string;
257
+ caseExternalKey: string;
258
+ experimentId: string;
259
+ attempt: number;
260
+ traceEvidence: "captured" | "omitted" | "not_requested";
261
+ traceExternalId: string | null;
262
+ omissionReason: string | null;
263
+ }
264
+ export interface Metric {
265
+ name: string;
266
+ value: boolean | number | string;
267
+ passed?: boolean;
268
+ }
269
+ export type Score = {
270
+ state: "scored";
271
+ metrics: Metric[];
272
+ explanation?: string;
273
+ evidence?: JsonValue;
274
+ } | {
275
+ state: "error";
276
+ error: TypedError;
277
+ } | {
278
+ state: "skipped";
279
+ explanation: string;
280
+ };
281
+ export type Result = Score & {
282
+ evaluationItemId: string;
283
+ scorerVersionId: string;
284
+ sourceDigest?: string;
285
+ };
286
+ export interface ScoreContext {
287
+ inputs: JsonValue;
288
+ output?: JsonValue;
289
+ expected?: JsonValue;
290
+ hasOutput: boolean;
291
+ hasExpected: boolean;
292
+ metadata: Record<string, JsonValue>;
293
+ executionState: TerminalState;
294
+ }
295
+ export interface LocalScorer {
296
+ definition: Extract<ScorerDefinition, {
297
+ kind: "local_code";
298
+ }>;
299
+ /** Trusted local code. There is no callback timeout or side-effect cancellation. */
300
+ score(context: ScoreContext): Score | Promise<Score>;
301
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export { createEvaluationClient, EvaluationClient, HueApiError } from "./evals/client.js";
2
+ export type { EvaluationClientOptions } from "./evals/client.js";
3
+ export { runExperiment, rescore, UncertainExecutionError, OutcomeSerializationError, } from "./evals/runner.js";
4
+ export type { RunExperimentOptions, RescoreOptions, RunnerReport } from "./evals/runner.js";
5
+ export { builtins, defineLocalScorer, scoreLocally } from "./evals/scorers.js";
6
+ export { sourceDigest } from "./evals/json.js";
7
+ export type * from "./evals/types.js";
package/dist/evals.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createEvaluationClient, EvaluationClient, HueApiError } from "./evals/client.js";
2
+ export { runExperiment, rescore, UncertainExecutionError, OutcomeSerializationError, } from "./evals/runner.js";
3
+ export { builtins, defineLocalScorer, scoreLocally } from "./evals/scorers.js";
4
+ export { sourceDigest } from "./evals/json.js";
@@ -0,0 +1,3 @@
1
+ export { createHue, HueClient, HueConnectionError, type ExistingHueProviders } from "./client.js";
2
+ export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
3
+ export type * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createHue, HueClient, HueConnectionError } from "./client.js";
2
+ export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
@@ -0,0 +1,7 @@
1
+ import type { ReadableSpan } from "@opentelemetry/sdk-trace";
2
+ import type { ReadableLogRecord } from "@opentelemetry/sdk-logs";
3
+ import { type Resource } from "@opentelemetry/resources";
4
+ import type { HueOptions } from "./types.js";
5
+ export type ResourceCache = WeakMap<Resource, Resource>;
6
+ export declare function redactSpan(span: ReadableSpan, options: HueOptions, cache: ResourceCache): ReadableSpan;
7
+ export declare function redactLog(log: ReadableLogRecord, options: HueOptions, cache: ResourceCache): ReadableLogRecord;