@openpond/evals 0.8.1 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +47 -0
  2. package/dist/deterministic-graders.js +110 -0
  3. package/dist/graders.js +9 -51
  4. package/dist/index.js +1 -0
  5. package/dist/metric-policy.js +50 -0
  6. package/dist/metrics-node.js +16 -0
  7. package/dist/metrics.js +105 -0
  8. package/dist/runs.js +12 -0
  9. package/dist/tasksets.js +5 -0
  10. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +14 -0
  11. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  12. package/dist/types/conformance.d.ts +28 -0
  13. package/dist/types/conformance.d.ts.map +1 -1
  14. package/dist/types/deterministic-graders.d.ts +23 -0
  15. package/dist/types/deterministic-graders.d.ts.map +1 -0
  16. package/dist/types/evidence/conformance.d.ts +4 -4
  17. package/dist/types/evidence/contracts.d.ts +8 -8
  18. package/dist/types/graders.d.ts +1 -0
  19. package/dist/types/graders.d.ts.map +1 -1
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/javascript-isolate.d.ts +1 -1
  23. package/dist/types/javascript-isolate.d.ts.map +1 -1
  24. package/dist/types/learning/admission.d.ts +3 -3
  25. package/dist/types/learning/contracts.d.ts +3 -3
  26. package/dist/types/learning/operations.d.ts +12 -12
  27. package/dist/types/learning/repository.d.ts +26 -4
  28. package/dist/types/learning/repository.d.ts.map +1 -1
  29. package/dist/types/learning/transport.d.ts +33 -11
  30. package/dist/types/learning/transport.d.ts.map +1 -1
  31. package/dist/types/metric-policy.d.ts +112 -0
  32. package/dist/types/metric-policy.d.ts.map +1 -0
  33. package/dist/types/metrics-node.d.ts +8 -0
  34. package/dist/types/metrics-node.d.ts.map +1 -0
  35. package/dist/types/metrics.d.ts +37 -0
  36. package/dist/types/metrics.d.ts.map +1 -0
  37. package/dist/types/preferences.d.ts +3 -3
  38. package/dist/types/rewards.d.ts +9 -9
  39. package/dist/types/runs.d.ts +88 -0
  40. package/dist/types/runs.d.ts.map +1 -1
  41. package/dist/types/tasksets.d.ts +44 -0
  42. package/dist/types/tasksets.d.ts.map +1 -1
  43. package/dist/types/telemetry-analysis.d.ts +1 -1
  44. package/dist/types/telemetry-bundle.d.ts +4 -4
  45. package/dist/types/telemetry.d.ts +3 -3
  46. package/package.json +9 -1
  47. package/schemas/learning/v1/package.schema.json +108 -0
package/README.md CHANGED
@@ -44,6 +44,53 @@ Subpath exports are available at `/harness`, `/tasksets`, `/benchmarks`, `/grade
44
44
  not a hosted client. It does not execute OpenPond Desktop or Sandbox sessions,
45
45
  resolve credentials, or persist artifacts.
46
46
 
47
+ ## Ordinary graders
48
+
49
+ `evaluateDeterministicGrader` and `portableDeterministicCheck` from `/graders`
50
+ provide the shared local/portable interpretation of ordinary checks. They retain
51
+ explicit exact-text normalization, final-answer extraction, structured JSON,
52
+ artifact and runtime-event requirements, and structural state comparisons.
53
+ Native test/diff checks declare their expected output field when exported.
54
+ Empty, unsupported or unavailable checks remain unscorable rather than earning
55
+ a zero or a passing reward. Desktop grade components preserve null scores and
56
+ display them as not scored.
57
+
58
+ ## Authored Taskset metrics
59
+
60
+ `@openpond/evals/metrics` exports the shared `TasksetMetricPolicySchema` and
61
+ `executeTasksetMetric`. Supply the policy from the pinned Taskset Release, its
62
+ Run Manifest, and verified attempt receipts. The result carries the policy,
63
+ policy hash, all input receipt references, population counts and a content hash.
64
+ It is separate from the evaluation's ordinary mean-score accounting.
65
+
66
+ The release's optional `metrics` field is the authoritative policy. For complete
67
+ evaluations, use `aggregateTasksetEvaluationReceipts` or the Node
68
+ `aggregateTasksetEvaluationInWorker` export. They bind the release and task IDs
69
+ to the manifest, calculate `authoredMetric`, and attach it only to the exact
70
+ receipt population. `meanScore` remains the ordinary mean. Hosts should capture
71
+ and validate custom source with `assertTasksetMetricSource` before model work,
72
+ then retain those bytes throughout the run. Complete Taskset packages require
73
+ the declared module as a private, content-matching file.
74
+
75
+ Mean score, explicit grader pass rate, weighted mean and custom aggregation use
76
+ one population rule: nonterminal attempts, infrastructure failures, timeouts and
77
+ cancellations are excluded. Other missing or ineligible rewards obey the
78
+ policy's `zero` or `exclude` setting. Pass rate requires an explicit boolean
79
+ `passed` verdict. Empty populations return `null`.
80
+
81
+ Weighted means require positive `taskWeights` keyed by task ID; each attempt
82
+ uses its task's declared weight. Missing weights are errors. Custom modules
83
+ export `aggregate(scores)` (or the declared export) and return a finite number
84
+ between zero and one. Exact UTF-8 source must match the policy's SHA-256 before
85
+ execution. Module source is limited to 512 KiB and custom inputs to 4 MiB.
86
+
87
+ Node hosts use `executeTasksetMetricInWorker` from `@openpond/evals/metrics/node`.
88
+ It strips type annotations from `.ts`, `.mts` and `.cts` modules, then executes
89
+ them in a fresh deterministic interpreter without host APIs or imports. Workers
90
+ stop before success, failure or cancellation is returned. Other hosts pass an
91
+ isolated executor to the portable function; authored source must never run in
92
+ the host JavaScript context.
93
+
47
94
  ## Benchmarks
48
95
 
49
96
  `BenchmarkDefinition` binds a named benchmark to an immutable Taskset Release,
@@ -0,0 +1,110 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { validateTaskSchema, validateTaskValue } from "./task-schema.js";
3
+ /** Local and portable execution interpret the same declared checks. Missing or
4
+ * unsupported checks are unscorable; an empty check cannot earn a reward. */
5
+ export function evaluateDeterministicGrader(input) {
6
+ const { grader, task, evidence } = input;
7
+ const config = grader.config;
8
+ if (grader.kind === "content") {
9
+ const outputField = text(config.outputField) ?? "text";
10
+ const actual = text(evidence.output[outputField]);
11
+ if (config.operator === "exact_equals") {
12
+ const expected = text(config.expectedValue);
13
+ if (expected === null)
14
+ return unavailable("Exact content grading requires an expected value.");
15
+ const normalize = (value) => {
16
+ const unicode = config.normalizeUnicode === true ? value.normalize("NFC") : value;
17
+ return config.trimWhitespace === true ? unicode.trim() : unicode;
18
+ };
19
+ return outcome(actual !== null && normalize(actual) === normalize(expected));
20
+ }
21
+ if (config.operator !== undefined && config.operator !== "final_answer_equals_expected")
22
+ return unavailable("Unsupported content grader operator.");
23
+ if (config.operator === undefined && (config.includes !== undefined || config.excludes !== undefined)) {
24
+ const includes = strings(config.includes), excludes = strings(config.excludes);
25
+ if (!includes.length && !excludes.length)
26
+ return unavailable("Content grading requires a nonempty check.");
27
+ const value = typeof evidence.output.text === "string" ? evidence.output.text : JSON.stringify(evidence.output);
28
+ return outcome(includes.every(item => value.includes(item)) && excludes.every(item => !value.includes(item)));
29
+ }
30
+ const expected = text(config.expectedValue) ?? text(task.expectedOutput?.[text(config.expectedField) ?? "text"]);
31
+ const normalize = config.operator === "final_answer_equals_expected" ? normalizeFinalAnswer : normalizeContent;
32
+ if (expected === null || !normalize(expected))
33
+ return unavailable("Content grading requires a nonempty expected value.");
34
+ return outcome(actual !== null && normalize(actual) === normalize(expected));
35
+ }
36
+ if (grader.kind === "schema") {
37
+ if (config.operator !== undefined && config.operator !== "json_schema_subset")
38
+ return unavailable("Unsupported schema grader operator.");
39
+ const schema = config.operator === "json_schema_subset" ? config.schema : config.jsonSchema;
40
+ if (schema !== undefined) {
41
+ const checked = validateTaskSchema(schema);
42
+ if (!checked.valid)
43
+ return unavailable(checked.issues[0].message);
44
+ let value = evidence.output;
45
+ if (config.operator === "json_schema_subset") {
46
+ const encoded = evidence.output[text(config.jsonField) ?? "text"];
47
+ if (typeof encoded === "string") {
48
+ try {
49
+ value = JSON.parse(encoded.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
50
+ }
51
+ catch {
52
+ return outcome(false, "The model output was not valid JSON.");
53
+ }
54
+ }
55
+ }
56
+ const checkedValue = validateTaskValue(schema, value);
57
+ return outcome(checkedValue.valid, checkedValue.valid ? undefined : checkedValue.issues.map(issue => issue.message).join(" "));
58
+ }
59
+ const required = strings(config.requiredKeys);
60
+ if (!required.length)
61
+ return unavailable("Schema grading requires a JSON Schema or explicit required keys.");
62
+ return outcome(required.every(key => Object.hasOwn(evidence.output, key)));
63
+ }
64
+ if (grader.kind === "artifact") {
65
+ const contains = text(config.refIncludes);
66
+ if (!contains)
67
+ return unavailable("Artifact grading requires a declared artifact reference.");
68
+ return outcome(evidence.artifactRefs.some(ref => ref.includes(contains)));
69
+ }
70
+ if (grader.kind === "runtime_event") {
71
+ const required = strings(config.requiredEvents);
72
+ if (!required.length)
73
+ return unavailable("Runtime event grading requires declared events.");
74
+ return outcome(required.every(event => evidence.runtimeEventRefs.some(ref => ref.includes(event))));
75
+ }
76
+ if (config.operator === "output_field_equals") {
77
+ const field = text(config.outputField);
78
+ if (!field || !Object.hasOwn(config, "expectedValue"))
79
+ return unavailable("Output comparison requires a field and expected value.");
80
+ return outcome(Object.hasOwn(evidence.output, field) && contentHash(evidence.output[field]) === contentHash(config.expectedValue));
81
+ }
82
+ if (config.operator !== undefined)
83
+ return unavailable("Unsupported state grader operator.");
84
+ const fields = strings(config.fields);
85
+ const compared = fields.length ? fields : Object.keys(task.expectedOutput ?? {});
86
+ if (!compared.length || compared.some(field => !task.expectedOutput || !Object.hasOwn(task.expectedOutput, field)))
87
+ return unavailable("State grading requires expected values for every compared field.");
88
+ return outcome(compared.every(field => Object.hasOwn(evidence.output, field) && contentHash(evidence.output[field]) === contentHash(task.expectedOutput[field])));
89
+ }
90
+ /** Encode native artifact/test checks without changing their meaning in transit. */
91
+ export function portableDeterministicCheck(grader) {
92
+ if (grader.kind === "file") {
93
+ const { pathIncludes, ...config } = grader.config;
94
+ return { kind: "artifact", config: { ...config, ...(pathIncludes === undefined ? {} : { refIncludes: pathIncludes }) } };
95
+ }
96
+ if (grader.kind === "diff" || grader.kind === "test")
97
+ return { kind: "state", config: { operator: "output_field_equals", outputField: grader.kind === "test" ? "testsPassed" : "diffAccepted", expectedValue: true } };
98
+ return { kind: grader.kind, config: grader.config };
99
+ }
100
+ function outcome(passed, feedback) { return { score: Number(passed), passed, feedback: feedback ?? (passed ? "Deterministic grader passed." : "Deterministic grader failed.") }; }
101
+ function unavailable(feedback) { return { score: null, passed: false, feedback }; }
102
+ function text(value) { return typeof value === "string" ? value : null; }
103
+ function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : []; }
104
+ function normalizeContent(value) { return value.normalize("NFKC").trim().replace(/[,,]/g, "").replace(/[.\s]+$/g, "").replace(/\s+/g, " "); }
105
+ function normalizeFinalAnswer(value) {
106
+ const boxed = [...value.matchAll(/\\boxed\{([^{}]+)\}/g)].at(-1)?.[1];
107
+ const hashAnswer = value.match(/####\s*([^\n\r]+)/)?.[1];
108
+ const label = value.match(/(?:final\s+answer|answer)\s*(?::|is|=)\s*([^\n\r]+)/i)?.[1];
109
+ return normalizeContent((boxed ?? hashAnswer ?? label ?? value).normalize("NFKC").trim().replace(/^\$+|\$+$/g, "").replace(/^\\\(|\\\)$/g, ""));
110
+ }
package/dist/graders.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { FailureClassSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash } from "@openpond/harness";
3
- import { validateTaskSchema, validateTaskValue } from "./task-schema.js";
3
+ import { evaluateDeterministicGrader } from "./deterministic-graders.js";
4
+ export { evaluateDeterministicGrader, portableDeterministicCheck } from "./deterministic-graders.js";
4
5
  export const GraderEvidenceContentSchema = z.object({
5
6
  schemaVersion: z.literal("openpond.graderEvidence.v1"),
6
7
  graderId: ReleaseIdSchema,
@@ -43,55 +44,15 @@ export async function gradeEvidence(input) {
43
44
  }));
44
45
  }
45
46
  function gradeDeterministic(grader, task, attempt) {
46
- let passed = false;
47
- const config = grader.config;
48
- if (grader.kind === "content") {
49
- const outputField = string(config.outputField) ?? "text";
50
- const actual = string(attempt.output[outputField]);
51
- const expected = string(config.expectedValue) ?? string(task.expectedOutput?.[string(config.expectedField) ?? "text"]);
52
- if (expected === null || !normalize(expected))
53
- return unavailable("Content grading requires a nonempty expected value.");
54
- passed = actual !== null && expected !== null && normalize(actual) === normalize(expected);
55
- }
56
- else if (grader.kind === "schema") {
57
- if (config.jsonSchema !== undefined) {
58
- const schema = validateTaskSchema(config.jsonSchema);
59
- if (!schema.valid)
60
- return unavailable(schema.issues[0].message);
61
- passed = validateTaskValue(config.jsonSchema, attempt.output).valid;
62
- }
63
- else {
64
- const required = strings(config.requiredKeys);
65
- if (!required.length)
66
- return unavailable("Schema grading requires a JSON Schema or explicit required keys.");
67
- passed = required.every((key) => Object.hasOwn(attempt.output, key));
68
- }
69
- }
70
- else if (grader.kind === "artifact") {
71
- const contains = string(config.refIncludes) ?? "";
72
- if (!contains)
73
- return unavailable("Artifact grading requires a declared artifact reference.");
74
- passed = attempt.artifactRefs.some((ref) => ref.includes(contains));
75
- }
76
- else if (grader.kind === "runtime_event") {
77
- const requiredEvents = strings(config.requiredEvents);
78
- if (!requiredEvents.length)
79
- return unavailable("Runtime event grading requires declared events.");
80
- passed = requiredEvents.every((required) => attempt.runtimeEventRefs.some((ref) => ref.includes(required)));
81
- }
82
- else {
83
- const fields = strings(config.fields);
84
- const compared = fields.length ? fields : Object.keys(task.expectedOutput ?? {});
85
- if (!compared.length || compared.some((field) => !task.expectedOutput || !Object.hasOwn(task.expectedOutput, field)))
86
- return unavailable("State grading requires expected values for every compared field.");
87
- passed = compared.every((field) => Object.hasOwn(attempt.output, field) && contentHash(attempt.output[field]) === contentHash(task.expectedOutput[field]));
88
- }
47
+ const result = evaluateDeterministicGrader({ grader, task, evidence: attempt });
48
+ if (result.score === null)
49
+ return unavailable(result.feedback);
89
50
  return {
90
- score: passed ? 1 : 0,
91
- passed,
51
+ score: result.score,
52
+ passed: result.passed,
92
53
  rewardEligible: grader.rewardEligible,
93
- failureClass: passed ? null : "policy_failure",
94
- feedback: [passed ? "Deterministic grader passed." : "Deterministic grader failed."],
54
+ failureClass: result.passed ? null : "policy_failure",
55
+ feedback: [result.feedback],
95
56
  visibleEvidenceRefs: [...attempt.runtimeEventRefs, ...attempt.artifactRefs],
96
57
  privilegedEvidenceRefs: grader.privileged ? [task.privilegedContextRef].filter((ref) => ref !== null) : [],
97
58
  };
@@ -109,6 +70,3 @@ function evidence(grader, result) {
109
70
  function unavailable(message) {
110
71
  return { score: null, passed: false, rewardEligible: false, failureClass: "grader_failure", feedback: [message], visibleEvidenceRefs: [], privilegedEvidenceRefs: [] };
111
72
  }
112
- function string(value) { return typeof value === "string" ? value : null; }
113
- function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : []; }
114
- function normalize(value) { return value.normalize("NFKC").trim().replace(/[,,]/g, "").replace(/[.\s]+$/g, "").replace(/\s+/g, " "); }
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./graders.js";
8
8
  export * from "./harness.js";
9
9
  export * from "./learned-preference.js";
10
10
  export * from "./runs.js";
11
+ export * from "./metrics.js";
11
12
  export * from "./model-improvement-qualification.js";
12
13
  export * from "./preferences.js";
13
14
  export * from "./review-conformance.js";
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { assertContentHash, contentHash, ImmutableReleaseRefSchema, ReleaseHashSchema } from "@openpond/harness";
3
+ const MetricIdSchema = z.string().trim().min(1).max(240);
4
+ const ModulePathSchema = z.string().trim().min(1).max(1_000).refine((value) => !/[\\:\0]/.test(value) && value.split("/").every((part) => part && part !== "." && part !== ".." && !part.startsWith(".env")), "Metric modules must use a safe relative path.");
5
+ export const TasksetMetricPolicySchema = z.object({
6
+ schemaVersion: z.literal("openpond.tasksetMetricPolicy.v1"),
7
+ primaryMetric: MetricIdSchema,
8
+ aggregation: z.enum(["mean_score", "pass_rate", "weighted_mean", "custom"]),
9
+ missingReward: z.enum(["zero", "exclude"]),
10
+ taskWeights: z.record(MetricIdSchema, z.number().positive().max(1_000_000)).optional(),
11
+ customAggregator: z.object({
12
+ module: ModulePathSchema,
13
+ exportName: z.string().trim().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/),
14
+ contentHash: ReleaseHashSchema,
15
+ timeoutMs: z.number().int().positive().max(300_000),
16
+ networkPolicy: z.literal("none"),
17
+ }).strict().nullable(),
18
+ }).strict().superRefine((policy, context) => {
19
+ if ((policy.aggregation === "custom") !== (policy.customAggregator !== null)) {
20
+ context.addIssue({ code: "custom", path: ["customAggregator"], message: "Only custom aggregation requires a content-hashed module." });
21
+ }
22
+ if (policy.aggregation === "weighted_mean" ? !policy.taskWeights || !Object.keys(policy.taskWeights).length : policy.taskWeights !== undefined) {
23
+ context.addIssue({ code: "custom", path: ["taskWeights"], message: "Only weighted means require explicit positive task weights." });
24
+ }
25
+ });
26
+ export const TasksetMetricResultContentSchema = z.object({
27
+ schemaVersion: z.literal("openpond.tasksetMetricResult.v1"),
28
+ runManifest: ImmutableReleaseRefSchema,
29
+ tasksetRelease: ImmutableReleaseRefSchema,
30
+ policy: TasksetMetricPolicySchema,
31
+ policyHash: ReleaseHashSchema,
32
+ receiptRefs: z.array(ImmutableReleaseRefSchema).min(1).max(1_000_000),
33
+ includedCount: z.number().int().nonnegative(),
34
+ missingRewardCount: z.number().int().nonnegative(),
35
+ excludedCount: z.number().int().nonnegative(),
36
+ value: z.number().min(0).max(1).nullable(),
37
+ }).strict();
38
+ export const TasksetMetricResultSchema = TasksetMetricResultContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
39
+ export function assertTasksetMetricResult(input) {
40
+ const result = TasksetMetricResultSchema.parse(input);
41
+ assertContentHash(result, "Taskset metric result");
42
+ if (result.policyHash !== contentHash(result.policy))
43
+ throw new Error("Taskset metric policy hash differs from its policy.");
44
+ if (result.includedCount + result.excludedCount !== result.receiptRefs.length
45
+ || (result.includedCount === 0) !== (result.value === null)
46
+ || result.missingRewardCount > (result.policy.missingReward === "zero" ? result.includedCount : result.excludedCount)
47
+ || new Set(result.receiptRefs.map(ref => ref.id)).size !== result.receiptRefs.length) {
48
+ throw new Error("Taskset metric population differs from its receipt references.");
49
+ }
50
+ }
@@ -0,0 +1,16 @@
1
+ import { stripTypeScriptTypes } from "node:module";
2
+ import { executeJavaScriptIsolateInWorker } from "./javascript-isolate-node.js";
3
+ import { aggregateTasksetEvaluationReceipts, executeTasksetMetric } from "./metrics.js";
4
+ /** Verifies original source before stripping TS, then waits for worker termination
5
+ * on success, error or cancellation. Authored imports and host APIs are unavailable. */
6
+ export function executeTasksetMetricInWorker(input) {
7
+ return executeTasksetMetric(input, executeMetric);
8
+ }
9
+ export function aggregateTasksetEvaluationInWorker(input) {
10
+ return aggregateTasksetEvaluationReceipts(input, executeMetric);
11
+ }
12
+ const executeMetric = ({ source, module, exportName, scores, timeoutMs, signal }) => executeJavaScriptIsolateInWorker({
13
+ source: /\.[cm]?ts$/.test(module) ? stripTypeScriptTypes(source, { mode: "strip" }) : source,
14
+ exportName, value: scores, timeoutMs, signal,
15
+ maxResultBytes: 1_024, deterministic: true, errorPrefix: "metric",
16
+ });
@@ -0,0 +1,105 @@
1
+ import { assertContentHash, contentHash, sha256 } from "@openpond/harness";
2
+ import { aggregateEvaluationReceipts, RunManifestSchema, verifyAttemptReceipt } from "./runs.js";
3
+ import { TasksetMetricPolicySchema, TasksetMetricResultContentSchema, TasksetMetricResultSchema } from "./metric-policy.js";
4
+ import { assertTasksetRelease } from "./tasksets.js";
5
+ export * from "./metric-policy.js";
6
+ /** Preflight the exact metric source before any model or tool execution. */
7
+ export function assertTasksetMetricSource(taskset, source) {
8
+ assertTasksetRelease(taskset);
9
+ if (taskset.metrics?.customAggregator)
10
+ assertMetricSource(taskset.metrics.customAggregator, source);
11
+ }
12
+ /** Bind the declared metric to one frozen release and population, retaining
13
+ * ordinary mean-score accounting independently of the authored calculation. */
14
+ export async function aggregateTasksetEvaluationReceipts(input, executor) {
15
+ assertTasksetMetricSource(input.taskset, input.source);
16
+ if (input.taskset.id !== input.manifest.tasksetRelease.id || input.taskset.contentHash !== input.manifest.tasksetRelease.contentHash)
17
+ throw new Error("Metric Taskset differs from the Run Manifest's pinned release.");
18
+ const taskIds = new Set(input.taskset.tasks.map(task => task.id));
19
+ if (input.receipts.some(receipt => !taskIds.has(receipt.taskId)))
20
+ throw new Error("Metric receipt names a task outside the pinned Taskset.");
21
+ const authoredMetric = input.taskset.metrics ? await executeTasksetMetric({ ...input, policy: input.taskset.metrics }, executor) : undefined;
22
+ return aggregateEvaluationReceipts({ id: input.id, manifest: input.manifest, receipts: input.receipts, metadata: input.metadata, authoredMetric });
23
+ }
24
+ /** Hosts supply the policy from the pinned Taskset, never a mutable form.
25
+ * Operational failures and nonterminal receipts are excluded for every policy.
26
+ * Missing rewards on the remaining receipts obey zero/exclude. A task's weight
27
+ * applies to each of its attempts; pass rate uses the grader's explicit verdict.
28
+ */
29
+ export async function executeTasksetMetric(input, executor) {
30
+ input.signal?.throwIfAborted();
31
+ const policy = TasksetMetricPolicySchema.parse(input.policy);
32
+ const manifest = RunManifestSchema.parse(input.manifest);
33
+ assertContentHash(manifest, "Metric Run Manifest");
34
+ if (!input.receipts.length || input.receipts.length > 1_000_000)
35
+ throw new Error("Metric execution requires 1 to 1,000,000 attempt receipts.");
36
+ const ids = new Set();
37
+ const scores = [];
38
+ const weights = [];
39
+ let missingRewardCount = 0;
40
+ for (const receipt of input.receipts) {
41
+ input.signal?.throwIfAborted();
42
+ if (!verifyAttemptReceipt(receipt))
43
+ throw new Error(`Metric receipt ${receipt.id} has an invalid content hash.`);
44
+ if (receipt.runManifest.id !== manifest.id || receipt.runManifest.contentHash !== manifest.contentHash)
45
+ throw new Error(`Metric receipt ${receipt.id} belongs to a different Run Manifest.`);
46
+ if (ids.has(receipt.id))
47
+ throw new Error(`Metric receipt ${receipt.id} is duplicated.`);
48
+ ids.add(receipt.id);
49
+ if (!receipt.terminal || ["infrastructure_failure", "timeout", "cancelled"].includes(receipt.failureClass ?? ""))
50
+ continue;
51
+ const score = receipt.metadata.score;
52
+ if (typeof score === "number" && (!Number.isFinite(score) || score < 0 || score > 1))
53
+ throw new Error(`Metric receipt ${receipt.id} has an invalid score.`);
54
+ const hasReward = receipt.metadata.rewardEligible === true && typeof score === "number"
55
+ && (policy.aggregation !== "pass_rate" || typeof receipt.metadata.passed === "boolean");
56
+ if (!hasReward) {
57
+ missingRewardCount += 1;
58
+ if (policy.missingReward === "exclude")
59
+ continue;
60
+ }
61
+ const weight = policy.aggregation === "weighted_mean" && Object.hasOwn(policy.taskWeights, receipt.taskId) ? policy.taskWeights[receipt.taskId] : undefined;
62
+ if (policy.aggregation === "weighted_mean" && weight === undefined)
63
+ throw new Error(`Metric policy has no weight for task ${receipt.taskId}.`);
64
+ scores.push(hasReward ? policy.aggregation === "pass_rate" ? Number(receipt.metadata.passed) : score : 0);
65
+ weights.push(weight ?? 1);
66
+ }
67
+ let value = null;
68
+ const receiptRefs = input.receipts.map(({ id, contentHash }) => ({ id, contentHash }));
69
+ const includedCount = scores.length;
70
+ if (policy.customAggregator) {
71
+ assertMetricSource(policy.customAggregator, input.source);
72
+ if (!executor)
73
+ throw new Error("Custom metrics require an isolated executor.");
74
+ // Empty populations remain unscorable, even if authored code would return a value.
75
+ if (scores.length) {
76
+ const result = await executor({ source: input.source, module: policy.customAggregator.module, exportName: policy.customAggregator.exportName, scores, timeoutMs: policy.customAggregator.timeoutMs, signal: input.signal });
77
+ if (typeof result !== "number" || !Number.isFinite(result) || result < 0 || result > 1)
78
+ throw new Error("Custom metric must return a finite number between 0 and 1.");
79
+ value = result;
80
+ }
81
+ }
82
+ else if (scores.length) {
83
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
84
+ value = scores.reduce((sum, score, index) => sum + score * (weights[index] / totalWeight), 0);
85
+ value = Math.max(0, Math.min(1, value));
86
+ }
87
+ input.signal?.throwIfAborted();
88
+ const content = TasksetMetricResultContentSchema.parse({
89
+ schemaVersion: "openpond.tasksetMetricResult.v1",
90
+ runManifest: { id: manifest.id, contentHash: manifest.contentHash },
91
+ tasksetRelease: manifest.tasksetRelease,
92
+ policy,
93
+ policyHash: contentHash(policy),
94
+ receiptRefs,
95
+ includedCount,
96
+ missingRewardCount,
97
+ excludedCount: receiptRefs.length - includedCount,
98
+ value,
99
+ });
100
+ return TasksetMetricResultSchema.parse({ ...content, contentHash: contentHash(content) });
101
+ }
102
+ function assertMetricSource(aggregator, source) {
103
+ if (source === undefined || new TextEncoder().encode(source).byteLength > 524_288 || sha256(source) !== aggregator.contentHash)
104
+ throw new Error("Metric module bytes do not match the pinned content hash or exceed the source limit.");
105
+ }
package/dist/runs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { FailureClassSchema, ImmutableArtifactRefSchema, ImmutableReleaseRefSchema, MetadataSchema, ModelRefSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, assertContentHash, contentHash, } from "@openpond/harness";
3
+ import { assertTasksetMetricResult, TasksetMetricResultSchema } from "./metric-policy.js";
3
4
  export const RuntimeTargetBindingSchema = z.object({
4
5
  adapterId: ReleaseIdSchema,
5
6
  placement: z.enum(["local", "remote", "colocated", "provider_native"]),
@@ -58,6 +59,7 @@ export const EvaluationResultContentSchema = z.object({
58
59
  terminalCount: z.number().int().nonnegative(),
59
60
  meanScore: z.number().min(0).max(1).nullable(),
60
61
  failureCounts: z.record(FailureClassSchema, z.number().int().nonnegative()),
62
+ authoredMetric: TasksetMetricResultSchema.optional(),
61
63
  metadata: MetadataSchema,
62
64
  }).strict();
63
65
  export const EvaluationResultSchema = EvaluationResultContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
@@ -101,6 +103,15 @@ export function aggregateEvaluationReceipts(input) {
101
103
  if (!verifyAttemptReceipt(receipt))
102
104
  throw new Error(`Attempt receipt ${receipt.id} has an invalid content hash.`);
103
105
  }
106
+ if (input.authoredMetric) {
107
+ assertTasksetMetricResult(input.authoredMetric);
108
+ const metric = input.authoredMetric;
109
+ if (metric.runManifest.id !== input.manifest.id || metric.runManifest.contentHash !== input.manifest.contentHash
110
+ || contentHash(metric.tasksetRelease) !== contentHash(input.manifest.tasksetRelease)
111
+ || contentHash(metric.receiptRefs) !== contentHash(input.receipts.map(({ id, contentHash }) => ({ id, contentHash })))) {
112
+ throw new Error("Authored metric does not describe this evaluation's exact manifest and receipts.");
113
+ }
114
+ }
104
115
  const scores = input.receipts.flatMap((receipt) => typeof receipt.metadata.score === "number" && Number.isFinite(receipt.metadata.score)
105
116
  ? [receipt.metadata.score]
106
117
  : []);
@@ -118,6 +129,7 @@ export function aggregateEvaluationReceipts(input) {
118
129
  terminalCount: input.receipts.filter((receipt) => receipt.terminal).length,
119
130
  meanScore: scores.length ? scores.reduce((total, score) => total + score, 0) / scores.length : null,
120
131
  failureCounts,
132
+ ...(input.authoredMetric ? { authoredMetric: input.authoredMetric } : {}),
121
133
  metadata: input.metadata ?? {},
122
134
  });
123
135
  return EvaluationResultSchema.parse({ ...content, contentHash: contentHash(content) });
package/dist/tasksets.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { CapabilityRequirementSchema, ImmutableAssetRefSchema, MetadataSchema, ReleaseHashSchema, ReleaseIdSchema, ToolDeclarationSchema, assertContentHash, contentHash, } from "@openpond/harness";
3
+ import { TasksetMetricPolicySchema } from "./metric-policy.js";
3
4
  export const TaskSplitSchema = z.enum(["train", "validation", "test", "frozen_eval"]);
4
5
  export const RequiredOutputContractSchema = z.object({
5
6
  path: z.string().trim().min(1).max(2_000).refine(safeRelativePath),
@@ -85,6 +86,7 @@ export const TasksetReleaseContentSchema = z.object({
85
86
  tasks: z.array(TaskRecordSchema).min(1).max(1_000_000),
86
87
  graders: z.array(GraderSpecSchema).min(1).max(1_000),
87
88
  verifierSetRelease: z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict().optional(),
89
+ metrics: TasksetMetricPolicySchema.optional(),
88
90
  metadata: MetadataSchema,
89
91
  }).strict();
90
92
  export const TasksetReleaseSchema = TasksetReleaseContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
@@ -115,6 +117,9 @@ export function validateTasksetRelease(input) {
115
117
  }
116
118
  const clusterSplits = new Map();
117
119
  for (const task of taskset.tasks) {
120
+ if (taskset.metrics?.aggregation === "weighted_mean" && !Object.hasOwn(taskset.metrics.taskWeights, task.id)) {
121
+ issues.push({ code: "metric_task_weight_missing", severity: "error", message: `Metric policy has no weight for task ${task.id}.`, path: "metrics.taskWeights" });
122
+ }
118
123
  const splits = clusterSplits.get(task.clusterKey) ?? new Set();
119
124
  splits.add(task.split);
120
125
  clusterSplits.set(task.clusterKey, splits);
@@ -143,6 +143,20 @@ export declare const harnessRefinerBenchmarkRelease: {
143
143
  id: string;
144
144
  contentHash: string;
145
145
  } | undefined;
146
+ metrics?: {
147
+ schemaVersion: "openpond.tasksetMetricPolicy.v1";
148
+ primaryMetric: string;
149
+ aggregation: "mean_score" | "pass_rate" | "weighted_mean" | "custom";
150
+ missingReward: "zero" | "exclude";
151
+ customAggregator: {
152
+ module: string;
153
+ exportName: string;
154
+ contentHash: string;
155
+ timeoutMs: number;
156
+ networkPolicy: "none";
157
+ } | null;
158
+ taskWeights?: Record<string, number> | undefined;
159
+ } | undefined;
146
160
  };
147
161
  export declare const harnessRefinerBenchmarkAssets: Readonly<Record<string, string>>;
148
162
  //# sourceMappingURL=harness-refiner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness-refiner.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-benchmarks/harness-refiner.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqqD1C,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASzE,CAAC"}
1
+ {"version":3,"file":"harness-refiner.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-benchmarks/harness-refiner.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqqD1C,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASzE,CAAC"}
@@ -253,6 +253,20 @@ export declare const genericToolConformance: {
253
253
  id: string;
254
254
  contentHash: string;
255
255
  } | undefined;
256
+ metrics?: {
257
+ schemaVersion: "openpond.tasksetMetricPolicy.v1";
258
+ primaryMetric: string;
259
+ aggregation: "mean_score" | "pass_rate" | "weighted_mean" | "custom";
260
+ missingReward: "zero" | "exclude";
261
+ customAggregator: {
262
+ module: string;
263
+ exportName: string;
264
+ contentHash: string;
265
+ timeoutMs: number;
266
+ networkPolicy: "none";
267
+ } | null;
268
+ taskWeights?: Record<string, number> | undefined;
269
+ } | undefined;
256
270
  };
257
271
  manifest: {
258
272
  schemaVersion: "openpond.runManifest.v1";
@@ -549,6 +563,20 @@ export declare const marketingPortfolioConformance: {
549
563
  id: string;
550
564
  contentHash: string;
551
565
  } | undefined;
566
+ metrics?: {
567
+ schemaVersion: "openpond.tasksetMetricPolicy.v1";
568
+ primaryMetric: string;
569
+ aggregation: "mean_score" | "pass_rate" | "weighted_mean" | "custom";
570
+ missingReward: "zero" | "exclude";
571
+ customAggregator: {
572
+ module: string;
573
+ exportName: string;
574
+ contentHash: string;
575
+ timeoutMs: number;
576
+ networkPolicy: "none";
577
+ } | null;
578
+ taskWeights?: Record<string, number> | undefined;
579
+ } | undefined;
552
580
  };
553
581
  manifest: {
554
582
  schemaVersion: "openpond.runManifest.v1";
@@ -1 +1 @@
1
- {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../../../src/conformance.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEjC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAGxC,CAAC"}
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../../../src/conformance.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEjC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAGxC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { DeterministicGraderSpec, TaskRecord } from "./tasksets.js";
2
+ export type DeterministicGrade = {
3
+ score: number | null;
4
+ passed: boolean;
5
+ feedback: string;
6
+ };
7
+ /** Local and portable execution interpret the same declared checks. Missing or
8
+ * unsupported checks are unscorable; an empty check cannot earn a reward. */
9
+ export declare function evaluateDeterministicGrader(input: {
10
+ grader: Pick<DeterministicGraderSpec, "kind" | "config">;
11
+ task: Pick<TaskRecord, "expectedOutput">;
12
+ evidence: {
13
+ output: Record<string, unknown>;
14
+ artifactRefs: string[];
15
+ runtimeEventRefs: string[];
16
+ };
17
+ }): DeterministicGrade;
18
+ /** Encode native artifact/test checks without changing their meaning in transit. */
19
+ export declare function portableDeterministicCheck(grader: {
20
+ kind: DeterministicGraderSpec["kind"] | "file" | "diff" | "test";
21
+ config: Record<string, unknown>;
22
+ }): Pick<DeterministicGraderSpec, "kind" | "config">;
23
+ //# sourceMappingURL=deterministic-graders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic-graders.d.ts","sourceRoot":"","sources":["../../../../src/deterministic-graders.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGzE,MAAM,MAAM,kBAAkB,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7F;6EAC6E;AAC7E,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,MAAM,EAAE,IAAI,CAAC,uBAAuB,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IACzD,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;IACzC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CACnG,GAAG,kBAAkB,CAoErB;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,uBAAuB,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,IAAI,CAAC,uBAAuB,EAAE,MAAM,GAAG,QAAQ,CAAC,CAO1M"}
@@ -32,7 +32,7 @@ export declare const completeWorkProcessTraceFixture: {
32
32
  durationMs: number | null;
33
33
  cpuTimeMs: number | null;
34
34
  memoryPeakBytes: number | null;
35
- errorClass: "validation" | "policy" | "unknown" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
35
+ errorClass: "unknown" | "policy" | "validation" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
36
36
  };
37
37
  }[];
38
38
  contentHash: string;
@@ -209,7 +209,7 @@ export declare const incompleteWorkProcessTraceFixture: {
209
209
  durationMs: number | null;
210
210
  cpuTimeMs: number | null;
211
211
  memoryPeakBytes: number | null;
212
- errorClass: "validation" | "policy" | "unknown" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
212
+ errorClass: "unknown" | "policy" | "validation" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
213
213
  };
214
214
  }[];
215
215
  contentHash: string;
@@ -415,7 +415,7 @@ export declare const workEvidenceConformance: {
415
415
  durationMs: number | null;
416
416
  cpuTimeMs: number | null;
417
417
  memoryPeakBytes: number | null;
418
- errorClass: "validation" | "policy" | "unknown" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
418
+ errorClass: "unknown" | "policy" | "validation" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
419
419
  };
420
420
  }[];
421
421
  contentHash: string;
@@ -648,7 +648,7 @@ export declare const workEvidenceConformance: {
648
648
  durationMs: number | null;
649
649
  cpuTimeMs: number | null;
650
650
  memoryPeakBytes: number | null;
651
- errorClass: "validation" | "policy" | "unknown" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
651
+ errorClass: "unknown" | "policy" | "validation" | "environment" | "cancelled" | "timeout" | "infrastructure" | null;
652
652
  };
653
653
  }[];
654
654
  contentHash: string;