@openpond/evals 0.5.0 → 0.7.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.
Files changed (118) hide show
  1. package/CONTRACT.md +13 -1
  2. package/LEARNING.md +106 -0
  3. package/README.md +28 -2
  4. package/conformance/telemetry/v1/invalid-batch.json +17 -0
  5. package/conformance/telemetry/v1/valid-batch.json +58 -0
  6. package/dist/graders.js +26 -5
  7. package/dist/index.js +8 -0
  8. package/dist/javascript-verifier-contract.js +7 -0
  9. package/dist/javascript-verifier-node.js +56 -0
  10. package/dist/javascript-verifier-worker-source.js +1 -0
  11. package/dist/javascript-verifier-worker.js +6 -0
  12. package/dist/javascript-verifier.js +64 -0
  13. package/dist/learned-preference.js +334 -0
  14. package/dist/learning/admission.js +209 -0
  15. package/dist/learning/assets.js +37 -0
  16. package/dist/learning/contracts.js +267 -0
  17. package/dist/learning/errors.js +12 -0
  18. package/dist/learning/grade-worker.js +99 -0
  19. package/dist/learning/index.js +9 -0
  20. package/dist/learning/operations.js +33 -0
  21. package/dist/learning/repository.js +48 -0
  22. package/dist/learning/service.js +367 -0
  23. package/dist/learning/transport.js +25 -0
  24. package/dist/preferences.js +77 -11
  25. package/dist/rewards.js +230 -0
  26. package/dist/task-schema.js +142 -0
  27. package/dist/tasksets.js +3 -0
  28. package/dist/telemetry/index.js +4 -0
  29. package/dist/telemetry-analysis.js +183 -0
  30. package/dist/telemetry-bundle.js +60 -0
  31. package/dist/telemetry-catalog.js +50 -0
  32. package/dist/telemetry.js +112 -0
  33. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +7 -0
  34. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  35. package/dist/types/conformance.d.ts +14 -0
  36. package/dist/types/conformance.d.ts.map +1 -1
  37. package/dist/types/execution-contracts.d.ts +14 -0
  38. package/dist/types/execution-contracts.d.ts.map +1 -1
  39. package/dist/types/graders.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +8 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/javascript-verifier-contract.d.ts +9 -0
  43. package/dist/types/javascript-verifier-contract.d.ts.map +1 -0
  44. package/dist/types/javascript-verifier-node.d.ts +5 -0
  45. package/dist/types/javascript-verifier-node.d.ts.map +1 -0
  46. package/dist/types/javascript-verifier-worker-source.d.ts +2 -0
  47. package/dist/types/javascript-verifier-worker.d.ts +2 -0
  48. package/dist/types/javascript-verifier-worker.d.ts.map +1 -0
  49. package/dist/types/javascript-verifier.d.ts +15 -0
  50. package/dist/types/javascript-verifier.d.ts.map +1 -0
  51. package/dist/types/learned-preference.d.ts +282 -0
  52. package/dist/types/learned-preference.d.ts.map +1 -0
  53. package/dist/types/learning/admission.d.ts +335 -0
  54. package/dist/types/learning/admission.d.ts.map +1 -0
  55. package/dist/types/learning/assets.d.ts +49 -0
  56. package/dist/types/learning/assets.d.ts.map +1 -0
  57. package/dist/types/learning/contracts.d.ts +2149 -0
  58. package/dist/types/learning/contracts.d.ts.map +1 -0
  59. package/dist/types/learning/errors.d.ts +8 -0
  60. package/dist/types/learning/errors.d.ts.map +1 -0
  61. package/dist/types/learning/grade-worker.d.ts +31 -0
  62. package/dist/types/learning/grade-worker.d.ts.map +1 -0
  63. package/dist/types/learning/index.d.ts +10 -0
  64. package/dist/types/learning/index.d.ts.map +1 -0
  65. package/dist/types/learning/operations.d.ts +2061 -0
  66. package/dist/types/learning/operations.d.ts.map +1 -0
  67. package/dist/types/learning/repository.d.ts +1143 -0
  68. package/dist/types/learning/repository.d.ts.map +1 -0
  69. package/dist/types/learning/service.d.ts +22 -0
  70. package/dist/types/learning/service.d.ts.map +1 -0
  71. package/dist/types/learning/transport.d.ts +2139 -0
  72. package/dist/types/learning/transport.d.ts.map +1 -0
  73. package/dist/types/preferences.d.ts +55 -7
  74. package/dist/types/preferences.d.ts.map +1 -1
  75. package/dist/types/review-conformance.d.ts +5 -5
  76. package/dist/types/rewards.d.ts +611 -0
  77. package/dist/types/rewards.d.ts.map +1 -0
  78. package/dist/types/task-schema.d.ts +18 -0
  79. package/dist/types/task-schema.d.ts.map +1 -0
  80. package/dist/types/tasksets.d.ts +28 -0
  81. package/dist/types/tasksets.d.ts.map +1 -1
  82. package/dist/types/telemetry/index.d.ts +5 -0
  83. package/dist/types/telemetry/index.d.ts.map +1 -0
  84. package/dist/types/telemetry-analysis.d.ts +116 -0
  85. package/dist/types/telemetry-analysis.d.ts.map +1 -0
  86. package/dist/types/telemetry-bundle.d.ts +309 -0
  87. package/dist/types/telemetry-bundle.d.ts.map +1 -0
  88. package/dist/types/telemetry-catalog.d.ts +269 -0
  89. package/dist/types/telemetry-catalog.d.ts.map +1 -0
  90. package/dist/types/telemetry.d.ts +266 -0
  91. package/dist/types/telemetry.d.ts.map +1 -0
  92. package/package.json +40 -3
  93. package/schemas/learning/v1/asset.schema.json +89 -0
  94. package/schemas/learning/v1/batch.schema.json +132 -0
  95. package/schemas/learning/v1/binding.schema.json +207 -0
  96. package/schemas/learning/v1/command-request.schema.json +2414 -0
  97. package/schemas/learning/v1/decision.schema.json +440 -0
  98. package/schemas/learning/v1/definition.schema.json +397 -0
  99. package/schemas/learning/v1/evidence.schema.json +317 -0
  100. package/schemas/learning/v1/example-submission.schema.json +253 -0
  101. package/schemas/learning/v1/feedback-submission.schema.json +114 -0
  102. package/schemas/learning/v1/feedback.schema.json +233 -0
  103. package/schemas/learning/v1/grade.schema.json +412 -0
  104. package/schemas/learning/v1/iteration.schema.json +236 -0
  105. package/schemas/learning/v1/package.schema.json +776 -0
  106. package/schemas/learning/v1/policy.schema.json +373 -0
  107. package/schemas/learning/v1/read-request.schema.json +101 -0
  108. package/schemas/learning/v1/reward.schema.json +302 -0
  109. package/schemas/learning/v1/source.schema.json +198 -0
  110. package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
  111. package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
  112. package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
  113. package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
  114. package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
  115. package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
  116. package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
  117. package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
  118. package/schemas/telemetry/v1/telemetry-export-bundle.schema.json +655 -0
@@ -0,0 +1,230 @@
1
+ import { LearningDomainError } from "./learning/errors.js";
2
+ import { z } from "zod";
3
+ import { ImmutableAssetRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash } from "@openpond/harness";
4
+ import { gradeEvidence } from "./graders.js";
5
+ import { CustomVerifierGraderSpecSchema, DeterministicGraderSpecSchema, GraderSpecSchema, HumanGraderSpecSchema, ModelJudgeGraderSpecSchema } from "./tasksets.js";
6
+ import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema.js";
7
+ const bindingFields = { id: true, version: true, weight: true, hardGate: true, rewardEligible: true, privileged: true };
8
+ export const RewardImplementationSchema = z.union([
9
+ DeterministicGraderSpecSchema.omit(bindingFields),
10
+ ModelJudgeGraderSpecSchema.omit(bindingFields),
11
+ CustomVerifierGraderSpecSchema.omit(bindingFields),
12
+ HumanGraderSpecSchema.omit(bindingFields),
13
+ z.object({ kind: z.literal("learned_model"), modelVersion: ImmutableReleaseRefSchema, inputContract: ImmutableAssetRefSchema }).strict(),
14
+ ]);
15
+ export const RewardReleaseContentSchema = z.object({
16
+ schemaVersion: z.literal("openpond.rewardRelease.v1"),
17
+ id: ReleaseIdSchema,
18
+ revision: z.number().int().positive(),
19
+ name: z.string().trim().min(1).max(500),
20
+ description: z.string().max(10_000),
21
+ implementation: RewardImplementationSchema,
22
+ rawScore: z.object({ minimum: z.number().finite(), maximum: z.number().finite() }).strict(),
23
+ assets: z.array(ImmutableAssetRefSchema).max(1_000),
24
+ }).strict().superRefine((reward, context) => {
25
+ if (reward.rawScore.minimum >= reward.rawScore.maximum)
26
+ context.addIssue({ code: "custom", path: ["rawScore"], message: "A raw score contract requires minimum < maximum." });
27
+ if (reward.implementation.kind !== "learned_model" && (reward.rawScore.minimum !== 0 || reward.rawScore.maximum !== 1))
28
+ context.addIssue({ code: "custom", path: ["rawScore"], message: "Portable graders currently return scores in [0, 1]." });
29
+ if (reward.implementation.kind === "schema") {
30
+ const report = validateTaskSchema(reward.implementation.config.jsonSchema);
31
+ if (!report.valid)
32
+ context.addIssue({ code: "custom", path: ["implementation", "config", "jsonSchema"], message: report.issues[0].message });
33
+ }
34
+ });
35
+ export const RewardReleaseSchema = RewardReleaseContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
36
+ export const RewardReleaseRefSchema = ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).strict();
37
+ export const RewardNormalizationSchema = z.discriminatedUnion("kind", [
38
+ z.object({ kind: z.literal("identity") }).strict(),
39
+ z.object({ kind: z.literal("linear"), minimum: z.number().finite(), maximum: z.number().finite(), direction: z.enum(["higher", "lower"]) }).strict(),
40
+ ]);
41
+ export const RewardBindingSourceSchema = z.object({
42
+ graderId: ReleaseIdSchema,
43
+ reward: RewardReleaseRefSchema,
44
+ role: z.enum(["training", "evaluation"]),
45
+ normalization: RewardNormalizationSchema,
46
+ weight: z.number().nonnegative().max(1_000),
47
+ required: z.boolean(),
48
+ hardGate: z.boolean(),
49
+ privileged: z.boolean(),
50
+ fixtureRefs: z.array(ImmutableReleaseRefSchema).max(1_000),
51
+ }).strict();
52
+ export const RewardBindingContentSchema = z.object({
53
+ schemaVersion: z.literal("openpond.rewardBinding.v1"),
54
+ id: ReleaseIdSchema,
55
+ revision: z.number().int().positive(),
56
+ name: z.string().trim().min(1).max(500).optional(),
57
+ description: z.string().max(10_000).optional(),
58
+ recipeRef: RewardReleaseRefSchema.optional(),
59
+ sources: z.array(RewardBindingSourceSchema).min(1).max(100),
60
+ aggregation: z.literal("weighted_mean"),
61
+ unscorable: z.literal("exclude_optional_require_all_required"),
62
+ }).strict().superRefine((binding, context) => {
63
+ if (new Set(binding.sources.map((source) => source.graderId)).size !== binding.sources.length)
64
+ context.addIssue({ code: "custom", path: ["sources"], message: "Bound grader identities must be unique." });
65
+ if (!binding.sources.some((source) => source.weight > 0))
66
+ context.addIssue({ code: "custom", path: ["sources"], message: "A binding needs at least one positively weighted source." });
67
+ for (const [index, source] of binding.sources.entries()) {
68
+ if (source.hardGate && !source.required)
69
+ context.addIssue({ code: "custom", path: ["sources", index], message: "A hard gate must be required." });
70
+ if (source.normalization.kind === "linear" && source.normalization.minimum >= source.normalization.maximum)
71
+ context.addIssue({ code: "custom", path: ["sources", index, "normalization"], message: "Linear normalization requires minimum < maximum." });
72
+ }
73
+ });
74
+ export const RewardBindingSchema = RewardBindingContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
75
+ export const BoundRewardResultSchema = z.object({
76
+ graderId: ReleaseIdSchema,
77
+ reward: RewardReleaseRefSchema,
78
+ role: z.enum(["training", "evaluation"]),
79
+ status: z.enum(["scored", "pending", "unavailable", "failed"]),
80
+ rawScore: z.number().finite().nullable(),
81
+ normalizedScore: z.number().min(0).max(1).nullable(),
82
+ passed: z.boolean().nullable(),
83
+ evidenceHashes: z.array(ReleaseHashSchema).max(1_000),
84
+ message: z.string().max(20_000).nullable(),
85
+ }).strict();
86
+ export const RewardCompositionContentSchema = z.object({
87
+ schemaVersion: z.literal("openpond.rewardComposition.v1"),
88
+ binding: RewardReleaseRefSchema,
89
+ taskHash: ReleaseHashSchema,
90
+ outputHash: ReleaseHashSchema,
91
+ results: z.array(BoundRewardResultSchema).min(1).max(100),
92
+ training: z.object({ status: z.enum(["scored", "unscorable", "not_configured"]), score: z.number().min(0).max(1).nullable(), passed: z.boolean().nullable() }).strict(),
93
+ evaluation: z.object({ status: z.enum(["scored", "unscorable", "not_configured"]), score: z.number().min(0).max(1).nullable(), passed: z.boolean().nullable() }).strict(),
94
+ }).strict();
95
+ export const RewardCompositionSchema = RewardCompositionContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
96
+ export function createRewardRelease(input) {
97
+ assertBoundedTaskJson(input);
98
+ const content = RewardReleaseContentSchema.parse(input);
99
+ return RewardReleaseSchema.parse({ ...content, contentHash: contentHash(content) });
100
+ }
101
+ export function createRewardBinding(input, rewards) {
102
+ const content = RewardBindingContentSchema.parse(input);
103
+ const binding = RewardBindingSchema.parse({ ...content, contentHash: contentHash(content) });
104
+ resolveBoundRewards(binding, rewards);
105
+ return binding;
106
+ }
107
+ export function resolveBoundRewards(input, rewards) {
108
+ const binding = RewardBindingSchema.parse(input);
109
+ assertRewardHash(binding);
110
+ return binding.sources.map((source) => {
111
+ const reward = rewards.find((candidate) => candidate.id === source.reward.id && candidate.revision === source.reward.revision && candidate.contentHash === source.reward.contentHash);
112
+ if (!reward)
113
+ throw new LearningDomainError("reward_release_missing", 422, source.graderId);
114
+ const parsed = RewardReleaseSchema.parse(reward);
115
+ assertRewardHash(parsed);
116
+ if (parsed.implementation.kind === "human" && source.role === "training")
117
+ throw new LearningDomainError("human_reward_not_executable", 422, source.graderId);
118
+ if (source.normalization.kind === "identity" && (parsed.rawScore.minimum < 0 || parsed.rawScore.maximum > 1))
119
+ throw new LearningDomainError("reward_normalization_required", 422, source.graderId);
120
+ if (source.normalization.kind === "linear" && (source.normalization.minimum !== parsed.rawScore.minimum || source.normalization.maximum !== parsed.rawScore.maximum))
121
+ throw new LearningDomainError("reward_normalization_contract_mismatch", 422, source.graderId);
122
+ return { source, reward: parsed };
123
+ });
124
+ }
125
+ export function compileBoundGraders(binding, rewards) {
126
+ return resolveBoundRewards(binding, rewards).map(({ source, reward }) => {
127
+ if (reward.implementation.kind === "learned_model")
128
+ throw new LearningDomainError("learned_reward_requires_model_execution_adapter");
129
+ return compileSource(source, reward);
130
+ });
131
+ }
132
+ export async function executeRewardBinding(input) {
133
+ const resolved = resolveBoundRewards(input.binding, input.rewards);
134
+ const results = [];
135
+ for (const { source, reward } of resolved) {
136
+ input.signal?.throwIfAborted();
137
+ const result = { graderId: source.graderId, reward: source.reward, role: source.role, status: "unavailable", rawScore: null, normalizedScore: null, passed: null, evidenceHashes: [], message: null };
138
+ try {
139
+ if (reward.implementation.kind === "human") {
140
+ result.status = "pending";
141
+ result.message = "Human review is pending.";
142
+ }
143
+ else if (reward.implementation.kind === "learned_model") {
144
+ if (!input.learnedReward)
145
+ result.message = "Learned reward execution is not configured.";
146
+ else {
147
+ const scored = await input.learnedReward({ reward, task: input.task, evidence: input.evidence, signal: input.signal });
148
+ Object.assign(result, { status: "scored", ...scored });
149
+ }
150
+ }
151
+ else {
152
+ const [evidence] = await gradeEvidence({ task: input.task, evidence: input.evidence, graders: [compileSource(source, reward)], modelJudge: input.modelJudge, customVerifier: input.customVerifier });
153
+ if (!evidence)
154
+ throw new LearningDomainError("reward_evidence_missing");
155
+ Object.assign(result, { status: evidence.score === null ? "unavailable" : "scored", rawScore: evidence.score, passed: evidence.score === null ? null : evidence.passed, evidenceHashes: [evidence.contentHash], message: evidence.feedback.join("\n") });
156
+ }
157
+ if (result.status === "scored") {
158
+ if (result.rawScore === null || !Number.isFinite(result.rawScore) || result.rawScore < reward.rawScore.minimum || result.rawScore > reward.rawScore.maximum)
159
+ throw new LearningDomainError("reward_raw_score_outside_contract");
160
+ const normalization = source.normalization;
161
+ result.normalizedScore = normalization.kind === "identity" ? result.rawScore : (result.rawScore - normalization.minimum) / (normalization.maximum - normalization.minimum);
162
+ if (normalization.kind === "linear" && normalization.direction === "lower")
163
+ result.normalizedScore = 1 - result.normalizedScore;
164
+ }
165
+ }
166
+ catch (error) {
167
+ input.signal?.throwIfAborted();
168
+ Object.assign(result, { status: "failed", rawScore: null, normalizedScore: null, passed: null, message: error instanceof Error ? error.message : "Reward execution failed." });
169
+ }
170
+ results.push(BoundRewardResultSchema.parse(result));
171
+ }
172
+ return composeBoundRewards({ binding: input.binding, taskHash: contentHash(input.task), outputHash: contentHash(input.evidence), results });
173
+ }
174
+ export function composeBoundRewards(input) {
175
+ const binding = RewardBindingSchema.parse(input.binding);
176
+ assertRewardHash(binding);
177
+ const results = input.results.map((result) => BoundRewardResultSchema.parse(result));
178
+ if (results.length !== binding.sources.length || new Set(results.map((result) => result.graderId)).size !== results.length)
179
+ throw new LearningDomainError("reward_result_set_incomplete");
180
+ for (const source of binding.sources) {
181
+ const result = results.find((candidate) => candidate.graderId === source.graderId);
182
+ if (!result || contentHash(result.reward) !== contentHash(source.reward) || result.role !== source.role)
183
+ throw new LearningDomainError("reward_result_binding_mismatch");
184
+ if ((result.status === "scored") !== (result.rawScore !== null && result.normalizedScore !== null && result.passed !== null))
185
+ throw new LearningDomainError("reward_result_score_state_invalid");
186
+ if (result.status !== "scored" && (result.rawScore !== null || result.normalizedScore !== null || result.passed !== null))
187
+ throw new LearningDomainError("unscorable_reward_contains_score");
188
+ if (result.status === "scored") {
189
+ const transform = source.normalization;
190
+ const normalized = transform.kind === "identity" ? result.rawScore : (result.rawScore - transform.minimum) / (transform.maximum - transform.minimum);
191
+ const expected = transform.kind === "linear" && transform.direction === "lower" ? 1 - normalized : normalized;
192
+ if (Math.abs(expected - result.normalizedScore) > 1e-12)
193
+ throw new LearningDomainError("reward_normalization_mismatch");
194
+ }
195
+ }
196
+ const content = RewardCompositionContentSchema.parse({
197
+ schemaVersion: "openpond.rewardComposition.v1",
198
+ binding: { id: binding.id, revision: binding.revision, contentHash: binding.contentHash },
199
+ taskHash: input.taskHash,
200
+ outputHash: input.outputHash,
201
+ results,
202
+ training: aggregate("training"),
203
+ evaluation: aggregate("evaluation"),
204
+ });
205
+ return RewardCompositionSchema.parse({ ...content, contentHash: contentHash(content) });
206
+ function aggregate(role) {
207
+ const selected = binding.sources.filter((source) => source.role === role).map((source) => ({ source, result: results.find((candidate) => candidate.graderId === source.graderId) }));
208
+ if (!selected.length)
209
+ return { status: "not_configured", score: null, passed: null };
210
+ if (selected.some(({ source, result }) => source.required && result.status !== "scored"))
211
+ return { status: "unscorable", score: null, passed: null };
212
+ const scored = selected.filter(({ result }) => result.status === "scored");
213
+ const weight = scored.reduce((sum, { source }) => sum + source.weight, 0);
214
+ if (weight === 0)
215
+ return { status: "unscorable", score: null, passed: null };
216
+ const gated = scored.some(({ source, result }) => source.hardGate && !result.passed);
217
+ return { status: "scored", score: gated ? 0 : scored.reduce((sum, { source, result }) => sum + source.weight * result.normalizedScore, 0) / weight, passed: !gated && scored.every(({ result }) => result.passed) };
218
+ }
219
+ }
220
+ export function assertRewardHash(value) {
221
+ assertBoundedTaskJson(value);
222
+ if (contentHash(withoutRewardHash(value)) !== value.contentHash)
223
+ throw new LearningDomainError("reward_content_hash_mismatch");
224
+ }
225
+ function withoutRewardHash(value) { const { contentHash: _hash, ...content } = value; return content; }
226
+ function compileSource(source, reward) {
227
+ if (reward.implementation.kind === "learned_model")
228
+ throw new LearningDomainError("learned_reward_requires_model_execution_adapter");
229
+ return GraderSpecSchema.parse({ ...reward.implementation, id: source.graderId, version: String(reward.revision), weight: source.weight, hardGate: source.hardGate, rewardEligible: source.role === "training", privileged: source.privileged });
230
+ }
@@ -0,0 +1,142 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { contentHash } from "@openpond/harness";
3
+ export const TASK_JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
4
+ export const TASK_VALUE_MAX_BYTES = 1_048_576;
5
+ export const TASK_SCHEMA_MAX_BYTES = 32_768;
6
+ const validators = new Map();
7
+ const schemaMaps = new Set(["properties", "$defs", "dependentSchemas"]);
8
+ const schemaArrays = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
9
+ const schemaChildren = new Set(["items", "additionalProperties", "not", "if", "then", "else", "contains", "unevaluatedProperties", "unevaluatedItems", "propertyNames"]);
10
+ const unsupported = new Set(["pattern", "patternProperties", "format", "$id", "$anchor", "$dynamicAnchor", "$dynamicRef", "$recursiveRef", "$recursiveAnchor", "$async", "$data", "contentSchema"]);
11
+ /** Bound producer-controlled JSON before hashing, schema compilation or validation. */
12
+ export function assertBoundedTaskJson(value, maxBytes = TASK_VALUE_MAX_BYTES) {
13
+ const pending = [{ value, depth: 0 }];
14
+ const seen = new Set();
15
+ let nodes = 0;
16
+ while (pending.length) {
17
+ const item = pending.pop();
18
+ if (item.exit) {
19
+ seen.delete(item.value);
20
+ continue;
21
+ }
22
+ if (++nodes > 100_000 || item.depth > 48)
23
+ throw new Error("task_json_complexity_exceeded");
24
+ if (item.value === null || typeof item.value === "string" || typeof item.value === "boolean")
25
+ continue;
26
+ if (typeof item.value === "number" && Number.isFinite(item.value))
27
+ continue;
28
+ if (typeof item.value !== "object")
29
+ throw new Error("task_json_value_invalid");
30
+ if (seen.has(item.value))
31
+ throw new Error("task_json_cycle");
32
+ seen.add(item.value);
33
+ pending.push({ ...item, exit: true });
34
+ const prototype = Object.getPrototypeOf(item.value);
35
+ if (!Array.isArray(item.value) && prototype !== Object.prototype && prototype !== null)
36
+ throw new Error("task_json_object_invalid");
37
+ if (Object.getOwnPropertySymbols(item.value).length)
38
+ throw new Error("task_json_symbol_invalid");
39
+ if (Object.hasOwn(item.value, "toJSON"))
40
+ throw new Error("task_json_serializer_invalid");
41
+ if (Array.isArray(item.value) && (item.value.length > 10_000 || Object.keys(item.value).length !== item.value.length))
42
+ throw new Error("task_json_array_invalid");
43
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(item.value))) {
44
+ if (descriptor.get || descriptor.set)
45
+ throw new Error("task_json_accessor_invalid");
46
+ if (Array.isArray(item.value) && key !== "length" && (!/^(0|[1-9]\d*)$/u.test(key) || Number(key) >= item.value.length))
47
+ throw new Error("task_json_array_invalid");
48
+ if (descriptor.enumerable)
49
+ pending.push({ value: descriptor.value, depth: item.depth + 1 });
50
+ }
51
+ }
52
+ if (new TextEncoder().encode(JSON.stringify(value)).byteLength > maxBytes)
53
+ throw new Error("task_json_size_exceeded");
54
+ }
55
+ /** No remote resolution, custom code, data mutation, regex or format plugins. */
56
+ export function validateTaskSchema(schema) {
57
+ try {
58
+ validator(schema);
59
+ return { valid: true, issues: [] };
60
+ }
61
+ catch (error) {
62
+ return { valid: false, issues: [{ path: "", code: "task_schema_invalid", message: error instanceof Error ? error.message : "Task schema is invalid." }] };
63
+ }
64
+ }
65
+ export function validateTaskValue(schema, value) {
66
+ try {
67
+ assertBoundedTaskJson(value);
68
+ const validate = validator(schema);
69
+ const valid = validate(value);
70
+ return {
71
+ valid,
72
+ issues: valid ? [] : (validate.errors ?? []).slice(0, 20).map((error) => ({
73
+ path: error.instancePath,
74
+ code: error.keyword,
75
+ message: error.message ?? "Value does not match the task schema.",
76
+ })),
77
+ };
78
+ }
79
+ catch (error) {
80
+ return { valid: false, issues: [{ path: "", code: "task_validation_unavailable", message: error instanceof Error ? error.message : "Task validation failed." }] };
81
+ }
82
+ }
83
+ function validator(schema) {
84
+ assertBoundedTaskJson(schema, TASK_SCHEMA_MAX_BYTES);
85
+ inspectSchema(schema, schema, new Set(), 0, { remaining: 2_048 });
86
+ const hash = contentHash(schema);
87
+ const cached = validators.get(hash);
88
+ if (cached)
89
+ return cached;
90
+ const ajv = new Ajv2020({ allErrors: false, ownProperties: true, strict: true, strictTypes: false, strictTuples: false, strictRequired: false, validateFormats: false, inlineRefs: false, loopRequired: 32, loopEnum: 32 });
91
+ const compiled = ajv.compile(schema);
92
+ if (validators.size >= 64)
93
+ validators.delete(validators.keys().next().value);
94
+ validators.set(hash, compiled);
95
+ return compiled;
96
+ }
97
+ function inspectSchema(value, root, ancestors, depth, budget) {
98
+ if (--budget.remaining < 0)
99
+ throw new Error("Task schema reference expansion exceeds the validation budget.");
100
+ if (typeof value === "boolean")
101
+ return;
102
+ if (!value || typeof value !== "object" || Array.isArray(value))
103
+ throw new Error("A task schema must be an object or boolean.");
104
+ if (depth > 24 || ancestors.has(value))
105
+ throw new Error("Recursive or deeply nested task schemas are not supported.");
106
+ const next = new Set(ancestors).add(value);
107
+ const schema = value;
108
+ if (schema.$schema !== undefined && schema.$schema !== TASK_JSON_SCHEMA_DIALECT)
109
+ throw new Error(`Task schemas must use ${TASK_JSON_SCHEMA_DIALECT}.`);
110
+ for (const [key, child] of Object.entries(schema)) {
111
+ if (unsupported.has(key))
112
+ throw new Error(`Task schema keyword ${key} is not supported by the bounded validator.`);
113
+ if (key === "$ref") {
114
+ if (typeof child !== "string" || !child.startsWith("#/$defs/"))
115
+ throw new Error("Task schema references must be local JSON pointers into $defs.");
116
+ let target = root;
117
+ for (const segment of child.slice(2).split("/")) {
118
+ if (/~(?![01])/u.test(segment))
119
+ throw new Error("Invalid task schema JSON pointer.");
120
+ const decoded = segment.replaceAll("~1", "/").replaceAll("~0", "~");
121
+ if (!target || typeof target !== "object" || !Object.hasOwn(target, decoded))
122
+ throw new Error(`Task schema reference ${child} is missing.`);
123
+ target = target[decoded];
124
+ }
125
+ inspectSchema(target, root, next, depth + 1, budget);
126
+ }
127
+ else if (schemaMaps.has(key)) {
128
+ if (!child || typeof child !== "object" || Array.isArray(child))
129
+ throw new Error(`Invalid schema map ${key}.`);
130
+ for (const entry of Object.values(child))
131
+ inspectSchema(entry, root, next, depth + 1, budget);
132
+ }
133
+ else if (schemaArrays.has(key)) {
134
+ if (!Array.isArray(child) || child.length > 32)
135
+ throw new Error(`Schema ${key} must contain at most 32 alternatives.`);
136
+ for (const entry of child)
137
+ inspectSchema(entry, root, next, depth + 1, budget);
138
+ }
139
+ else if (schemaChildren.has(key))
140
+ inspectSchema(child, root, next, depth + 1, budget);
141
+ }
142
+ }
package/dist/tasksets.js CHANGED
@@ -40,10 +40,13 @@ export const ModelJudgeGraderSpecSchema = GraderBaseSchema.extend({
40
40
  kind: z.literal("model_judge"),
41
41
  rubricRef: ImmutableAssetRefSchema,
42
42
  calibrationStatus: z.enum(["pending", "passed", "failed"]),
43
+ model: z.object({ providerId: ReleaseIdSchema, modelId: ReleaseIdSchema, revision: z.string().trim().min(1).max(500).nullable() }).strict().optional(),
44
+ temperature: z.number().min(0).max(2).optional(),
43
45
  }).strict();
44
46
  export const CustomVerifierGraderSpecSchema = GraderBaseSchema.extend({
45
47
  kind: z.literal("custom_verifier"),
46
48
  verifierRef: ImmutableAssetRefSchema,
49
+ exportName: z.string().trim().min(1).max(240).optional(),
47
50
  timeoutMs: z.number().int().positive().max(300_000),
48
51
  networkPolicy: z.literal("none"),
49
52
  }).strict();
@@ -0,0 +1,4 @@
1
+ export * from "../telemetry.js";
2
+ export * from "../telemetry-catalog.js";
3
+ export * from "../telemetry-analysis.js";
4
+ export * from "../telemetry-bundle.js";
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
3
+ import { getCoreMetricDefinition, validateCoreMetricObservation } from "./telemetry-catalog.js";
4
+ import { MetricObservationSchema, RunTelemetryEventSchema, TelemetryVisibilitySchema, } from "./telemetry.js";
5
+ export const TelemetryCohortSchema = z.object({
6
+ checkpointIds: z.array(ReleaseIdSchema).max(1_000),
7
+ steps: z.array(z.number().int().nonnegative()).max(10_000),
8
+ scenarioIds: z.array(ReleaseIdSchema).max(100_000),
9
+ rolloutGroupIds: z.array(ReleaseIdSchema).max(100_000),
10
+ attemptIds: z.array(ReleaseIdSchema).max(1_000_000),
11
+ splits: z.array(z.string().trim().min(1).max(100)).max(32),
12
+ failureOwners: z.array(z.string().trim().min(1).max(100)).max(32),
13
+ graders: z.array(z.string().trim().min(1).max(200)).max(1_000),
14
+ rewardEligible: z.boolean().nullable(),
15
+ }).strict();
16
+ export const EvidenceReferenceSchema = z.object({
17
+ id: ReleaseIdSchema,
18
+ contentHash: ReleaseHashSchema,
19
+ kind: z.enum(["rollout", "attempt", "trace", "grader", "checkpoint", "artifact"]),
20
+ visibility: TelemetryVisibilitySchema,
21
+ }).strict();
22
+ export const EvidenceCompletenessSchema = z.object({
23
+ schemaVersion: z.literal("openpond.telemetryEvidenceCompleteness.v1"),
24
+ runId: ReleaseIdSchema,
25
+ status: z.enum(["complete", "partial", "missing"]),
26
+ expectedEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
27
+ observedEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
28
+ missingEventTypes: z.array(z.string().trim().min(1).max(100)).max(100),
29
+ lastSequence: z.number().int().nonnegative().nullable(),
30
+ sequenceGaps: z.array(z.number().int().nonnegative()).max(10_000),
31
+ }).strict();
32
+ export const MetricSeriesPointSchema = z.object({
33
+ step: z.number().int().nonnegative().nullable(),
34
+ observedAt: ReleaseTimestampSchema,
35
+ value: z.number().finite(),
36
+ sampleCount: z.number().int().positive(),
37
+ }).strict();
38
+ export const RunMetricSummarySchema = z.object({
39
+ schemaVersion: z.literal("openpond.runMetricSummary.v1"),
40
+ runId: ReleaseIdSchema,
41
+ metricId: ReleaseIdSchema,
42
+ aggregation: z.enum(["last", "sum", "mean", "min", "max", "p50", "p95"]),
43
+ value: z.number().finite().nullable(),
44
+ sampleCount: z.number().int().nonnegative(),
45
+ series: z.array(MetricSeriesPointSchema).max(100_000),
46
+ }).strict();
47
+ export function createRunTelemetryEvent(input) {
48
+ const idHash = contentHash({ runId: input.lineage.runId, sequence: input.sequence, type: input.type, source: input.source });
49
+ return RunTelemetryEventSchema.parse({
50
+ schemaVersion: "openpond.runTelemetryEvent.v1",
51
+ eventId: `telemetry-${idHash.slice(0, 32)}`,
52
+ ...input,
53
+ attributes: input.attributes ?? {},
54
+ });
55
+ }
56
+ export function createMetricObservation(input) {
57
+ const idHash = contentHash({ runId: input.lineage.runId, metricId: input.metricId, sequence: input.sequence });
58
+ return validateCoreMetricObservation(MetricObservationSchema.parse({
59
+ schemaVersion: "openpond.metricObservation.v1",
60
+ observationId: `metric-${idHash.slice(0, 32)}`,
61
+ ...input,
62
+ dimensions: input.dimensions ?? {},
63
+ }));
64
+ }
65
+ export function telemetryIdempotencyKey(item) {
66
+ const id = item.schemaVersion === "openpond.metricObservation.v1" ? item.observationId : item.eventId;
67
+ return `${item.lineage.runId}:${item.sequence}:${id}`;
68
+ }
69
+ export function mergeTelemetryItems(input) {
70
+ const accepted = new Map();
71
+ const sequences = new Map();
72
+ for (const item of [...input.events, ...input.observations]) {
73
+ const key = telemetryIdempotencyKey(item);
74
+ const existing = accepted.get(key);
75
+ if (existing) {
76
+ if (contentHash(existing) !== contentHash(item))
77
+ throw new Error(`Telemetry idempotency conflict: ${key}`);
78
+ continue;
79
+ }
80
+ const sequenceKey = `${item.lineage.runId}:${item.sequence}`;
81
+ const priorKey = sequences.get(sequenceKey);
82
+ if (priorKey && priorKey !== key)
83
+ throw new Error(`Telemetry sequence conflict: ${sequenceKey}`);
84
+ sequences.set(sequenceKey, key);
85
+ accepted.set(key, item);
86
+ }
87
+ const sorted = [...accepted.values()].sort((left, right) => left.sequence - right.sequence);
88
+ return {
89
+ events: sorted.filter((item) => item.schemaVersion === "openpond.runTelemetryEvent.v1"),
90
+ observations: sorted.filter((item) => item.schemaVersion === "openpond.metricObservation.v1"),
91
+ };
92
+ }
93
+ function matchesOptionalSet(values, candidate) {
94
+ return values.length === 0 || (candidate !== null && values.includes(candidate));
95
+ }
96
+ export function filterTelemetryCohort(input) {
97
+ const cohort = TelemetryCohortSchema.parse(input.cohort);
98
+ const lineageMatches = (lineage) => matchesOptionalSet(cohort.checkpointIds, lineage.checkpointId)
99
+ && matchesOptionalSet(cohort.steps, lineage.step)
100
+ && matchesOptionalSet(cohort.scenarioIds, lineage.scenarioId)
101
+ && matchesOptionalSet(cohort.rolloutGroupIds, lineage.rolloutGroupId)
102
+ && matchesOptionalSet(cohort.attemptIds, lineage.attemptId);
103
+ const attributeMatches = (values) => matchesOptionalSet(cohort.splits, typeof values.split === "string" ? values.split : null)
104
+ && matchesOptionalSet(cohort.failureOwners, typeof values.failureOwner === "string" ? values.failureOwner : null)
105
+ && matchesOptionalSet(cohort.graders, typeof values.grader === "string" ? values.grader : null)
106
+ && (cohort.rewardEligible === null || values.rewardEligible === cohort.rewardEligible);
107
+ return {
108
+ events: input.events.filter((event) => lineageMatches(event.lineage) && attributeMatches(event.attributes)),
109
+ observations: input.observations.filter((observation) => lineageMatches(observation.lineage) && attributeMatches(observation.dimensions)),
110
+ };
111
+ }
112
+ export function deriveEvidenceCompleteness(input) {
113
+ const events = input.events.filter((event) => event.lineage.runId === input.runId).sort((left, right) => left.sequence - right.sequence);
114
+ const observed = [...new Set(events.map((event) => event.type))].sort();
115
+ const missing = [...new Set(input.expectedEventTypes)].filter((type) => !observed.includes(type)).sort();
116
+ const sequenceValues = [...new Set(events.map((event) => event.sequence))].sort((left, right) => left - right);
117
+ const lastSequence = events.at(-1)?.sequence ?? null;
118
+ const sequenceGaps = [];
119
+ let expected = 0;
120
+ for (const sequence of sequenceValues) {
121
+ while (expected < sequence && sequenceGaps.length < 10_000)
122
+ sequenceGaps.push(expected++);
123
+ expected = sequence + 1;
124
+ }
125
+ return EvidenceCompletenessSchema.parse({
126
+ schemaVersion: "openpond.telemetryEvidenceCompleteness.v1",
127
+ runId: input.runId,
128
+ status: events.length === 0 ? "missing" : missing.length || sequenceGaps.length ? "partial" : "complete",
129
+ expectedEventTypes: [...new Set(input.expectedEventTypes)].sort(),
130
+ observedEventTypes: observed,
131
+ missingEventTypes: missing,
132
+ lastSequence,
133
+ sequenceGaps,
134
+ });
135
+ }
136
+ function aggregate(values, method) {
137
+ if (!values.length)
138
+ return null;
139
+ if (method === "last")
140
+ return values.at(-1) ?? null;
141
+ if (method === "sum")
142
+ return values.reduce((total, value) => total + value, 0);
143
+ if (method === "mean")
144
+ return values.reduce((total, value) => total + value, 0) / values.length;
145
+ if (method === "min")
146
+ return Math.min(...values);
147
+ if (method === "max")
148
+ return Math.max(...values);
149
+ const sorted = [...values].sort((left, right) => left - right);
150
+ const percentile = method === "p50" ? 0.5 : 0.95;
151
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentile) - 1)] ?? null;
152
+ }
153
+ export function summarizeMetric(runId, metricId, observations) {
154
+ const definition = getCoreMetricDefinition(metricId);
155
+ if (!definition)
156
+ throw new Error(`Unknown core metric: ${metricId}`);
157
+ const selected = observations.filter((item) => item.lineage.runId === runId && item.metricId === metricId).sort((left, right) => left.sequence - right.sequence);
158
+ const buckets = new Map();
159
+ for (const item of selected) {
160
+ const key = item.lineage.step === null ? `time:${item.observedAt}` : `step:${item.lineage.step}`;
161
+ buckets.set(key, [...(buckets.get(key) ?? []), item]);
162
+ }
163
+ const series = [...buckets.values()].map((items) => ({
164
+ step: items[0]?.lineage.step ?? null,
165
+ observedAt: items[0]?.observedAt,
166
+ value: aggregate(items.map((item) => item.value), definition.aggregation),
167
+ sampleCount: items.length,
168
+ })).filter((point) => point.value !== null && point.observedAt !== undefined);
169
+ return RunMetricSummarySchema.parse({ schemaVersion: "openpond.runMetricSummary.v1", runId, metricId, aggregation: definition.aggregation, value: aggregate(selected.map((item) => item.value), definition.aggregation), sampleCount: selected.length, series });
170
+ }
171
+ export function redactTelemetryEvent(event, maximumVisibility) {
172
+ const rank = { policy_visible: 0, team_visible: 1, host_private: 2 };
173
+ if (rank[event.visibility] > rank[maximumVisibility])
174
+ return null;
175
+ return RunTelemetryEventSchema.parse(event);
176
+ }
177
+ export function redactTelemetryAttributes(event, deniedKeys) {
178
+ const denied = new Set(deniedKeys);
179
+ return RunTelemetryEventSchema.parse({
180
+ ...event,
181
+ attributes: Object.fromEntries(Object.entries(event.attributes).filter(([key]) => !denied.has(key))),
182
+ });
183
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
3
+ import { MetricCatalogSchema } from "./telemetry-catalog.js";
4
+ import { deriveEvidenceCompleteness, EvidenceCompletenessSchema, EvidenceReferenceSchema, redactTelemetryAttributes, redactTelemetryEvent } from "./telemetry-analysis.js";
5
+ import { MetricObservationSchema, RunTelemetryEventSchema } from "./telemetry.js";
6
+ export const TelemetryExportBundleContentSchema = z.object({
7
+ schemaVersion: z.literal("openpond.telemetryExportBundle.v1"),
8
+ id: ReleaseIdSchema,
9
+ runId: ReleaseIdSchema,
10
+ exportedAt: ReleaseTimestampSchema,
11
+ definitions: MetricCatalogSchema,
12
+ events: z.array(RunTelemetryEventSchema).max(1_000_000),
13
+ observations: z.array(MetricObservationSchema).max(10_000_000),
14
+ evidenceRefs: z.array(EvidenceReferenceSchema).max(1_000_000),
15
+ completeness: EvidenceCompletenessSchema,
16
+ }).strict();
17
+ export const TelemetryExportBundleSchema = TelemetryExportBundleContentSchema.extend({
18
+ contentHash: ReleaseHashSchema,
19
+ }).strict();
20
+ export function createTelemetryExportBundle(input) {
21
+ const content = TelemetryExportBundleContentSchema.parse(input);
22
+ if (content.completeness.runId !== content.runId) {
23
+ throw new Error("Telemetry export completeness belongs to another Run.");
24
+ }
25
+ if (content.events.some((event) => event.lineage.runId !== content.runId) || content.observations.some((observation) => observation.lineage.runId !== content.runId)) {
26
+ throw new Error("Telemetry export bundle contains evidence from another Run.");
27
+ }
28
+ return TelemetryExportBundleSchema.parse({ ...content, contentHash: contentHash(content) });
29
+ }
30
+ export function verifyTelemetryExportBundle(input) {
31
+ const parsed = TelemetryExportBundleSchema.safeParse(input);
32
+ if (!parsed.success)
33
+ return false;
34
+ const { contentHash: actual, ...content } = parsed.data;
35
+ return contentHash(TelemetryExportBundleContentSchema.parse(content)) === actual;
36
+ }
37
+ export function redactTelemetryExportBundle(input) {
38
+ const bundle = TelemetryExportBundleSchema.parse(input.bundle);
39
+ if (!verifyTelemetryExportBundle(bundle))
40
+ throw new Error("Telemetry export bundle has an invalid content hash.");
41
+ const rank = { policy_visible: 0, team_visible: 1, host_private: 2 };
42
+ const definitions = bundle.definitions.filter((definition) => rank[definition.visibility] <= rank[input.maximumVisibility]);
43
+ const metricIds = new Set(definitions.map((definition) => definition.id));
44
+ const events = bundle.events.flatMap((event) => {
45
+ const visible = redactTelemetryEvent(event, input.maximumVisibility);
46
+ return visible ? [redactTelemetryAttributes(visible, input.deniedAttributeKeys ?? [])] : [];
47
+ });
48
+ const expectedEventTypes = bundle.completeness.expectedEventTypes.filter((type) => events.some((event) => event.type === type));
49
+ return createTelemetryExportBundle({
50
+ schemaVersion: "openpond.telemetryExportBundle.v1",
51
+ id: input.id,
52
+ runId: bundle.runId,
53
+ exportedAt: input.exportedAt,
54
+ definitions,
55
+ events,
56
+ observations: bundle.observations.filter((observation) => metricIds.has(observation.metricId)),
57
+ evidenceRefs: bundle.evidenceRefs.filter((reference) => rank[reference.visibility] <= rank[input.maximumVisibility]),
58
+ completeness: deriveEvidenceCompleteness({ runId: bundle.runId, events, expectedEventTypes: expectedEventTypes }),
59
+ });
60
+ }