@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.
- package/CONTRACT.md +13 -1
- package/LEARNING.md +106 -0
- package/README.md +28 -2
- package/conformance/telemetry/v1/invalid-batch.json +17 -0
- package/conformance/telemetry/v1/valid-batch.json +58 -0
- package/dist/graders.js +26 -5
- package/dist/index.js +8 -0
- package/dist/javascript-verifier-contract.js +7 -0
- package/dist/javascript-verifier-node.js +56 -0
- package/dist/javascript-verifier-worker-source.js +1 -0
- package/dist/javascript-verifier-worker.js +6 -0
- package/dist/javascript-verifier.js +64 -0
- package/dist/learned-preference.js +334 -0
- package/dist/learning/admission.js +209 -0
- package/dist/learning/assets.js +37 -0
- package/dist/learning/contracts.js +267 -0
- package/dist/learning/errors.js +12 -0
- package/dist/learning/grade-worker.js +99 -0
- package/dist/learning/index.js +9 -0
- package/dist/learning/operations.js +33 -0
- package/dist/learning/repository.js +48 -0
- package/dist/learning/service.js +367 -0
- package/dist/learning/transport.js +25 -0
- package/dist/preferences.js +77 -11
- package/dist/rewards.js +230 -0
- package/dist/task-schema.js +142 -0
- package/dist/tasksets.js +3 -0
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry-analysis.js +183 -0
- package/dist/telemetry-bundle.js +60 -0
- package/dist/telemetry-catalog.js +50 -0
- package/dist/telemetry.js +112 -0
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts +7 -0
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
- package/dist/types/conformance.d.ts +14 -0
- package/dist/types/conformance.d.ts.map +1 -1
- package/dist/types/execution-contracts.d.ts +14 -0
- package/dist/types/execution-contracts.d.ts.map +1 -1
- package/dist/types/graders.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/javascript-verifier-contract.d.ts +9 -0
- package/dist/types/javascript-verifier-contract.d.ts.map +1 -0
- package/dist/types/javascript-verifier-node.d.ts +5 -0
- package/dist/types/javascript-verifier-node.d.ts.map +1 -0
- package/dist/types/javascript-verifier-worker-source.d.ts +2 -0
- package/dist/types/javascript-verifier-worker.d.ts +2 -0
- package/dist/types/javascript-verifier-worker.d.ts.map +1 -0
- package/dist/types/javascript-verifier.d.ts +15 -0
- package/dist/types/javascript-verifier.d.ts.map +1 -0
- package/dist/types/learned-preference.d.ts +282 -0
- package/dist/types/learned-preference.d.ts.map +1 -0
- package/dist/types/learning/admission.d.ts +335 -0
- package/dist/types/learning/admission.d.ts.map +1 -0
- package/dist/types/learning/assets.d.ts +49 -0
- package/dist/types/learning/assets.d.ts.map +1 -0
- package/dist/types/learning/contracts.d.ts +2149 -0
- package/dist/types/learning/contracts.d.ts.map +1 -0
- package/dist/types/learning/errors.d.ts +8 -0
- package/dist/types/learning/errors.d.ts.map +1 -0
- package/dist/types/learning/grade-worker.d.ts +31 -0
- package/dist/types/learning/grade-worker.d.ts.map +1 -0
- package/dist/types/learning/index.d.ts +10 -0
- package/dist/types/learning/index.d.ts.map +1 -0
- package/dist/types/learning/operations.d.ts +2061 -0
- package/dist/types/learning/operations.d.ts.map +1 -0
- package/dist/types/learning/repository.d.ts +1143 -0
- package/dist/types/learning/repository.d.ts.map +1 -0
- package/dist/types/learning/service.d.ts +22 -0
- package/dist/types/learning/service.d.ts.map +1 -0
- package/dist/types/learning/transport.d.ts +2139 -0
- package/dist/types/learning/transport.d.ts.map +1 -0
- package/dist/types/preferences.d.ts +55 -7
- package/dist/types/preferences.d.ts.map +1 -1
- package/dist/types/review-conformance.d.ts +5 -5
- package/dist/types/rewards.d.ts +611 -0
- package/dist/types/rewards.d.ts.map +1 -0
- package/dist/types/task-schema.d.ts +18 -0
- package/dist/types/task-schema.d.ts.map +1 -0
- package/dist/types/tasksets.d.ts +28 -0
- package/dist/types/tasksets.d.ts.map +1 -1
- package/dist/types/telemetry/index.d.ts +5 -0
- package/dist/types/telemetry/index.d.ts.map +1 -0
- package/dist/types/telemetry-analysis.d.ts +116 -0
- package/dist/types/telemetry-analysis.d.ts.map +1 -0
- package/dist/types/telemetry-bundle.d.ts +309 -0
- package/dist/types/telemetry-bundle.d.ts.map +1 -0
- package/dist/types/telemetry-catalog.d.ts +269 -0
- package/dist/types/telemetry-catalog.d.ts.map +1 -0
- package/dist/types/telemetry.d.ts +266 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/package.json +40 -3
- package/schemas/learning/v1/asset.schema.json +89 -0
- package/schemas/learning/v1/batch.schema.json +132 -0
- package/schemas/learning/v1/binding.schema.json +207 -0
- package/schemas/learning/v1/command-request.schema.json +2414 -0
- package/schemas/learning/v1/decision.schema.json +440 -0
- package/schemas/learning/v1/definition.schema.json +397 -0
- package/schemas/learning/v1/evidence.schema.json +317 -0
- package/schemas/learning/v1/example-submission.schema.json +253 -0
- package/schemas/learning/v1/feedback-submission.schema.json +114 -0
- package/schemas/learning/v1/feedback.schema.json +233 -0
- package/schemas/learning/v1/grade.schema.json +412 -0
- package/schemas/learning/v1/iteration.schema.json +236 -0
- package/schemas/learning/v1/package.schema.json +776 -0
- package/schemas/learning/v1/policy.schema.json +373 -0
- package/schemas/learning/v1/read-request.schema.json +101 -0
- package/schemas/learning/v1/reward.schema.json +302 -0
- package/schemas/learning/v1/source.schema.json +198 -0
- package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
- package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
- package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
- package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
- package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
- package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
- package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
- package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
- package/schemas/telemetry/v1/telemetry-export-bundle.schema.json +655 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { LearningDomainError } from "./errors.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ImmutableAssetRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
|
|
4
|
+
import { RewardBindingSchema, RewardCompositionSchema, RewardReleaseRefSchema, RewardReleaseSchema } from "../rewards.js";
|
|
5
|
+
import { TaskSplitSchema, TasksetReleaseContentSchema } from "../tasksets.js";
|
|
6
|
+
import { assertBoundedTaskJson, validateTaskSchema } from "../task-schema.js";
|
|
7
|
+
export const LearningJsonObjectSchema = z.record(z.string(), z.json());
|
|
8
|
+
export const LearningRevisionRefSchema = RewardReleaseRefSchema;
|
|
9
|
+
export const LearningJsonPointerSchema = z.string().max(2_000).refine((value) => value === "" || (value.startsWith("/") && !/~(?![01])/u.test(value)), "Use an RFC 6901 JSON pointer.");
|
|
10
|
+
export const TaskDefinitionContentSchema = z.object({
|
|
11
|
+
schemaVersion: z.literal("openpond.taskDefinition.v1"),
|
|
12
|
+
id: ReleaseIdSchema,
|
|
13
|
+
revision: z.number().int().positive(),
|
|
14
|
+
name: z.string().trim().min(1).max(500),
|
|
15
|
+
description: z.string().max(10_000),
|
|
16
|
+
instructions: z.string().trim().min(1).max(20_000),
|
|
17
|
+
category: z.enum(["structured", "question_answering", "coding", "tool_workflow", "custom"]),
|
|
18
|
+
familyNamespace: ReleaseIdSchema,
|
|
19
|
+
inputSchema: z.record(z.string(), z.unknown()),
|
|
20
|
+
outputSchema: z.record(z.string(), z.unknown()),
|
|
21
|
+
rewardBinding: LearningRevisionRefSchema,
|
|
22
|
+
harness: ImmutableReleaseRefSchema.nullable(),
|
|
23
|
+
execution: TasksetReleaseContentSchema.pick({ policy: true, environment: true, environmentRelease: true, tools: true, capabilities: true, verifierSetRelease: true }).strict(),
|
|
24
|
+
}).strict().superRefine((definition, context) => {
|
|
25
|
+
for (const field of ["inputSchema", "outputSchema"]) {
|
|
26
|
+
const schema = definition[field];
|
|
27
|
+
if (schema.type !== "object")
|
|
28
|
+
context.addIssue({ code: "custom", path: [field], message: "Task input/output use object envelopes; declare type: object." });
|
|
29
|
+
const report = validateTaskSchema(schema);
|
|
30
|
+
if (!report.valid)
|
|
31
|
+
context.addIssue({ code: "custom", path: [field], message: report.issues[0].message });
|
|
32
|
+
}
|
|
33
|
+
if (Boolean(definition.execution.environmentRelease) !== Boolean(definition.execution.verifierSetRelease))
|
|
34
|
+
context.addIssue({ code: "custom", path: ["execution"], message: "Bind both the environment and verifier-set releases." });
|
|
35
|
+
});
|
|
36
|
+
export const TaskDefinitionSchema = TaskDefinitionContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
|
|
37
|
+
export const TaskSourceMappingSchema = z.object({
|
|
38
|
+
schemaVersion: z.literal("openpond.taskSourceMapping.v1"),
|
|
39
|
+
exampleId: LearningJsonPointerSchema,
|
|
40
|
+
attemptId: LearningJsonPointerSchema,
|
|
41
|
+
occurredAt: LearningJsonPointerSchema,
|
|
42
|
+
input: LearningJsonPointerSchema,
|
|
43
|
+
observedOutput: LearningJsonPointerSchema.nullable(),
|
|
44
|
+
expected: LearningJsonPointerSchema.nullable(),
|
|
45
|
+
evaluatorContext: LearningJsonPointerSchema.nullable(),
|
|
46
|
+
familyKey: LearningJsonPointerSchema,
|
|
47
|
+
split: TaskSplitSchema,
|
|
48
|
+
}).strict();
|
|
49
|
+
export const LearningSourceContentSchema = z.object({
|
|
50
|
+
schemaVersion: z.literal("openpond.learningSource.v1"),
|
|
51
|
+
id: ReleaseIdSchema,
|
|
52
|
+
revision: z.number().int().positive(),
|
|
53
|
+
name: z.string().trim().min(1).max(500),
|
|
54
|
+
kind: z.enum(["direct", "benchmark", "work", "opentelemetry", "provider"]),
|
|
55
|
+
taskDefinition: LearningRevisionRefSchema,
|
|
56
|
+
enabled: z.boolean(),
|
|
57
|
+
allowedSplits: z.array(TaskSplitSchema).min(1).max(4),
|
|
58
|
+
mapping: TaskSourceMappingSchema.nullable(),
|
|
59
|
+
adapterVersion: z.string().trim().min(1).max(200).nullable(),
|
|
60
|
+
}).strict().superRefine((source, context) => {
|
|
61
|
+
if (source.kind !== "direct" && !source.mapping)
|
|
62
|
+
context.addIssue({ code: "custom", path: ["mapping"], message: "Mapped sources require an explicit mapping." });
|
|
63
|
+
if (source.mapping && !source.allowedSplits.includes(source.mapping.split))
|
|
64
|
+
context.addIssue({ code: "custom", path: ["allowedSplits"], message: "The mapping split must be allowed by this source." });
|
|
65
|
+
if (["opentelemetry", "provider"].includes(source.kind) && !source.adapterVersion)
|
|
66
|
+
context.addIssue({ code: "custom", path: ["adapterVersion"], message: "Trace adapters require a pinned version." });
|
|
67
|
+
});
|
|
68
|
+
export const LearningSourceSchema = LearningSourceContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
|
|
69
|
+
export const TaskExampleSubmissionSchema = z.object({
|
|
70
|
+
schemaVersion: z.literal("openpond.taskExample.v1"),
|
|
71
|
+
sourceId: ReleaseIdSchema,
|
|
72
|
+
idempotencyKey: ReleaseIdSchema,
|
|
73
|
+
taskDefinition: LearningRevisionRefSchema,
|
|
74
|
+
exampleId: ReleaseIdSchema,
|
|
75
|
+
attemptId: ReleaseIdSchema,
|
|
76
|
+
occurredAt: ReleaseTimestampSchema,
|
|
77
|
+
familyKey: ReleaseIdSchema.nullable(),
|
|
78
|
+
split: TaskSplitSchema,
|
|
79
|
+
input: LearningJsonObjectSchema,
|
|
80
|
+
observedOutput: LearningJsonObjectSchema.nullable(),
|
|
81
|
+
expected: LearningJsonObjectSchema.nullable(),
|
|
82
|
+
evaluatorContext: LearningJsonObjectSchema.nullable(),
|
|
83
|
+
assets: z.array(ImmutableAssetRefSchema).max(1_000),
|
|
84
|
+
provenance: z.object({ sourceRecordRef: z.string().max(2_000).nullable(), mappingHash: ReleaseHashSchema.nullable() }).strict(),
|
|
85
|
+
}).strict();
|
|
86
|
+
export const TaskEvidenceContentSchema = z.object({
|
|
87
|
+
schemaVersion: z.literal("openpond.taskEvidence.v1"),
|
|
88
|
+
id: ReleaseIdSchema,
|
|
89
|
+
revision: z.number().int().positive(),
|
|
90
|
+
source: LearningRevisionRefSchema,
|
|
91
|
+
submission: TaskExampleSubmissionSchema,
|
|
92
|
+
supersedes: LearningRevisionRefSchema.nullable(),
|
|
93
|
+
correctionFeedbackId: ReleaseIdSchema.nullable(),
|
|
94
|
+
receivedAt: ReleaseTimestampSchema,
|
|
95
|
+
}).strict();
|
|
96
|
+
export const TaskEvidenceSchema = TaskEvidenceContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
|
|
97
|
+
export const TaskFeedbackSubmissionSchema = z.object({
|
|
98
|
+
schemaVersion: z.literal("openpond.taskFeedback.v1"),
|
|
99
|
+
sourceId: ReleaseIdSchema,
|
|
100
|
+
idempotencyKey: ReleaseIdSchema,
|
|
101
|
+
exampleId: ReleaseIdSchema,
|
|
102
|
+
attemptId: ReleaseIdSchema,
|
|
103
|
+
expectedEvidenceHash: ReleaseHashSchema.nullable(),
|
|
104
|
+
occurredAt: ReleaseTimestampSchema,
|
|
105
|
+
kind: z.enum(["outcome", "target_correction", "ground_truth_correction", "input_correction", "family_resolution"]),
|
|
106
|
+
value: LearningJsonObjectSchema,
|
|
107
|
+
note: z.string().max(20_000),
|
|
108
|
+
}).strict();
|
|
109
|
+
export const TaskFeedbackSchema = z.object({
|
|
110
|
+
schemaVersion: z.literal("openpond.taskFeedbackRecord.v1"),
|
|
111
|
+
id: ReleaseIdSchema,
|
|
112
|
+
submission: TaskFeedbackSubmissionSchema,
|
|
113
|
+
status: z.enum(["pending_example", "pending_review", "applied", "superseded", "rejected"]),
|
|
114
|
+
evidence: LearningRevisionRefSchema.nullable(),
|
|
115
|
+
createdAt: ReleaseTimestampSchema,
|
|
116
|
+
revision: z.number().int().positive(),
|
|
117
|
+
review: z.object({ actorId: ReleaseIdSchema, decision: LearningRevisionRefSchema.nullable(), note: z.string().max(20_000), resolvedAt: ReleaseTimestampSchema }).strict().nullable().default(null),
|
|
118
|
+
}).strict();
|
|
119
|
+
export const TaskAdmissionDecisionContentSchema = z.object({
|
|
120
|
+
schemaVersion: z.literal("openpond.taskAdmissionDecision.v1"),
|
|
121
|
+
id: ReleaseIdSchema,
|
|
122
|
+
revision: z.number().int().positive(),
|
|
123
|
+
evidence: LearningRevisionRefSchema,
|
|
124
|
+
supersedes: LearningRevisionRefSchema.nullable(),
|
|
125
|
+
actor: z.object({ kind: z.enum(["human", "qualified_policy"]), id: ReleaseIdSchema, policy: LearningRevisionRefSchema.nullable() }).strict(),
|
|
126
|
+
evidenceValidity: z.enum(["valid", "invalid", "pending"]),
|
|
127
|
+
taskAdmissibility: z.enum(["approved", "rejected", "pending"]),
|
|
128
|
+
observedQuality: z.enum(["passed", "failed", "unscored", "unavailable"]),
|
|
129
|
+
targetApproval: z.enum(["approved", "rejected", "pending", "not_required"]),
|
|
130
|
+
approvedTarget: LearningJsonObjectSchema.nullable(),
|
|
131
|
+
grade: RewardCompositionSchema.nullable(),
|
|
132
|
+
targetGrade: RewardCompositionSchema.nullable(),
|
|
133
|
+
note: z.string().max(20_000),
|
|
134
|
+
decidedAt: ReleaseTimestampSchema,
|
|
135
|
+
}).strict().superRefine((decision, context) => {
|
|
136
|
+
if (decision.targetApproval === "approved" && decision.approvedTarget === null)
|
|
137
|
+
context.addIssue({ code: "custom", path: ["approvedTarget"], message: "Target approval requires an actual target." });
|
|
138
|
+
if (decision.targetApproval !== "approved" && decision.approvedTarget !== null)
|
|
139
|
+
context.addIssue({ code: "custom", path: ["approvedTarget"], message: "Only approved targets may be selected for supervised learning." });
|
|
140
|
+
if (decision.actor.kind === "qualified_policy" && !decision.actor.policy)
|
|
141
|
+
context.addIssue({ code: "custom", path: ["actor"], message: "Automatic admission must pin its qualified policy." });
|
|
142
|
+
if (decision.taskAdmissibility === "approved" && decision.evidenceValidity !== "valid")
|
|
143
|
+
context.addIssue({ code: "custom", path: ["taskAdmissibility"], message: "Only valid evidence can produce an approved task." });
|
|
144
|
+
});
|
|
145
|
+
export const TaskAdmissionDecisionSchema = TaskAdmissionDecisionContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
|
|
146
|
+
export const TaskBatchContentSchema = z.object({
|
|
147
|
+
schemaVersion: z.literal("openpond.taskBatch.v1"),
|
|
148
|
+
id: ReleaseIdSchema,
|
|
149
|
+
revision: z.literal(1),
|
|
150
|
+
taskDefinition: LearningRevisionRefSchema,
|
|
151
|
+
rewardBinding: LearningRevisionRefSchema,
|
|
152
|
+
purpose: z.enum(["supervised_training", "reward_training", "evaluation"]),
|
|
153
|
+
examples: z.array(z.object({ evidence: LearningRevisionRefSchema, decision: LearningRevisionRefSchema, familyKey: ReleaseIdSchema, inputHash: ReleaseHashSchema, split: TaskSplitSchema }).strict()).min(1).max(10_000),
|
|
154
|
+
sealedAt: ReleaseTimestampSchema,
|
|
155
|
+
sealedBy: ReleaseIdSchema,
|
|
156
|
+
}).strict().superRefine((batch, context) => {
|
|
157
|
+
if (new Set(batch.examples.map((entry) => entry.evidence.id)).size !== batch.examples.length)
|
|
158
|
+
context.addIssue({ code: "custom", path: ["examples"], message: "Each evidence identity can occur only once in a batch." });
|
|
159
|
+
const splits = new Map();
|
|
160
|
+
for (const [index, entry] of batch.examples.entries()) {
|
|
161
|
+
if ((batch.purpose === "evaluation") === (entry.split === "train"))
|
|
162
|
+
context.addIssue({ code: "custom", path: ["examples", index, "split"], message: "Training uses train examples; evaluation uses held-out examples." });
|
|
163
|
+
for (const key of [`family:${entry.familyKey}`, `input:${entry.inputHash}`]) {
|
|
164
|
+
if (splits.has(key) && splits.get(key) !== entry.split)
|
|
165
|
+
context.addIssue({ code: "custom", path: ["examples", index], message: "A task family or identical input cannot cross splits." });
|
|
166
|
+
splits.set(key, entry.split);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
export const TaskBatchSchema = TaskBatchContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
|
|
171
|
+
export const LearningPolicyContentSchema = z.object({
|
|
172
|
+
schemaVersion: z.literal("openpond.learningPolicy.v1"),
|
|
173
|
+
id: ReleaseIdSchema,
|
|
174
|
+
revision: z.number().int().positive(),
|
|
175
|
+
modelProjectId: ReleaseIdSchema,
|
|
176
|
+
executionOwner: z.enum(["local", "hosted"]),
|
|
177
|
+
enabled: z.boolean(),
|
|
178
|
+
sources: z.array(LearningRevisionRefSchema).min(1).max(100),
|
|
179
|
+
taskDefinition: LearningRevisionRefSchema,
|
|
180
|
+
rewardBinding: LearningRevisionRefSchema,
|
|
181
|
+
admission: z.object({ mode: z.enum(["human", "qualified_automatic"]), qualification: ImmutableReleaseRefSchema.nullable(), minimumApprovedExamples: z.number().int().positive().max(100_000) }).strict(),
|
|
182
|
+
trigger: z.discriminatedUnion("kind", [
|
|
183
|
+
z.object({ kind: z.literal("manual") }).strict(),
|
|
184
|
+
z.object({ kind: z.literal("approved_count") }).strict(),
|
|
185
|
+
z.object({ kind: z.literal("schedule"), intervalSeconds: z.number().int().min(60).max(31_536_000) }).strict(),
|
|
186
|
+
z.object({ kind: z.literal("upstream_accepted"), modelProjectId: ReleaseIdSchema }).strict(),
|
|
187
|
+
]),
|
|
188
|
+
trainingParent: ImmutableReleaseRefSchema,
|
|
189
|
+
teacher: ImmutableReleaseRefSchema.nullable(),
|
|
190
|
+
training: z.object({ method: z.enum(["sft", "dpo", "grpo", "ppo", "sdft", "opd", "opsd", "sdpo"]), recipe: ImmutableReleaseRefSchema, retentionEvaluation: ImmutableReleaseRefSchema, replayBatches: z.array(ImmutableReleaseRefSchema).max(100) }).strict(),
|
|
191
|
+
limits: z.object({ maxIterationSpendUsd: z.number().positive().max(100_000), maxDailySpendUsd: z.number().positive().max(1_000_000), cooldownSeconds: z.number().int().nonnegative().max(31_536_000), maxRetries: z.number().int().nonnegative().max(10), maxBatchExamples: z.number().int().positive().max(10_000), maxBacklogExamples: z.number().int().positive().max(1_000_000) }).strict(),
|
|
192
|
+
automation: z.object({ collect: z.boolean(), train: z.boolean(), accept: z.boolean(), serve: z.boolean() }).strict(),
|
|
193
|
+
acceptance: z.object({ minimumScore: z.number().min(0).max(1), maximumRetentionRegression: z.number().min(0).max(1), requireImprovement: z.boolean(), rollbackVersion: ImmutableReleaseRefSchema.nullable() }).strict(),
|
|
194
|
+
}).strict().superRefine((policy, context) => {
|
|
195
|
+
if (policy.admission.mode === "qualified_automatic" && !policy.admission.qualification)
|
|
196
|
+
context.addIssue({ code: "custom", path: ["admission"], message: "Automatic admission requires qualification evidence." });
|
|
197
|
+
if (policy.automation.serve && !policy.automation.accept)
|
|
198
|
+
context.addIssue({ code: "custom", path: ["automation"], message: "Automatic serving requires configured acceptance." });
|
|
199
|
+
if (policy.automation.accept && !policy.acceptance.rollbackVersion)
|
|
200
|
+
context.addIssue({ code: "custom", path: ["acceptance"], message: "Automatic acceptance requires a rollback version." });
|
|
201
|
+
if (policy.trigger.kind === "upstream_accepted" && policy.trigger.modelProjectId === policy.modelProjectId)
|
|
202
|
+
context.addIssue({ code: "custom", path: ["trigger"], message: "A model cannot trigger itself through upstream acceptance." });
|
|
203
|
+
if (policy.limits.maxBatchExamples < policy.admission.minimumApprovedExamples)
|
|
204
|
+
context.addIssue({ code: "custom", path: ["limits"], message: "The batch limit must permit the minimum approved example count." });
|
|
205
|
+
if (policy.limits.maxDailySpendUsd < policy.limits.maxIterationSpendUsd)
|
|
206
|
+
context.addIssue({ code: "custom", path: ["limits"], message: "Daily spend must permit the iteration budget." });
|
|
207
|
+
});
|
|
208
|
+
export const LearningPolicySchema = LearningPolicyContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
|
|
209
|
+
export const LearningIterationStatusSchema = z.enum(["waiting_for_data", "waiting_for_review", "ready", "training", "evaluating", "candidate_ready", "accepted", "rejected", "paused", "failed", "cancelling", "cancelled"]);
|
|
210
|
+
export const LearningIterationSchema = z.object({
|
|
211
|
+
schemaVersion: z.literal("openpond.learningIteration.v1"),
|
|
212
|
+
id: ReleaseIdSchema,
|
|
213
|
+
revision: z.number().int().positive(),
|
|
214
|
+
policy: LearningRevisionRefSchema,
|
|
215
|
+
status: LearningIterationStatusSchema,
|
|
216
|
+
triggerIdentity: ReleaseIdSchema,
|
|
217
|
+
batch: LearningRevisionRefSchema.nullable(),
|
|
218
|
+
sourceWatermarks: z.record(ReleaseIdSchema, z.number().int().nonnegative()),
|
|
219
|
+
trainingParent: ImmutableReleaseRefSchema,
|
|
220
|
+
teacher: ImmutableReleaseRefSchema.nullable(),
|
|
221
|
+
upstreamEvent: ImmutableReleaseRefSchema.nullable(),
|
|
222
|
+
trainingJob: ImmutableReleaseRefSchema.nullable(),
|
|
223
|
+
evaluationJob: ImmutableReleaseRefSchema.nullable(),
|
|
224
|
+
candidateVersion: ImmutableReleaseRefSchema.nullable(),
|
|
225
|
+
dispatchId: ReleaseIdSchema,
|
|
226
|
+
retryCount: z.number().int().nonnegative(),
|
|
227
|
+
spendUsd: z.number().nonnegative(),
|
|
228
|
+
failure: z.object({ code: ReleaseIdSchema, message: z.string().max(20_000) }).strict().nullable(),
|
|
229
|
+
createdAt: ReleaseTimestampSchema,
|
|
230
|
+
updatedAt: ReleaseTimestampSchema,
|
|
231
|
+
}).strict();
|
|
232
|
+
export const TaskGradeRunSchema = z.object({
|
|
233
|
+
schemaVersion: z.literal("openpond.taskGradeRun.v1"),
|
|
234
|
+
id: ReleaseIdSchema,
|
|
235
|
+
revision: z.number().int().positive(),
|
|
236
|
+
evidence: LearningRevisionRefSchema,
|
|
237
|
+
binding: LearningRevisionRefSchema,
|
|
238
|
+
target: z.enum(["observed", "proposed_target"]),
|
|
239
|
+
output: LearningJsonObjectSchema,
|
|
240
|
+
status: z.enum(["queued", "running", "completed", "failed", "cancelling", "cancelled"]),
|
|
241
|
+
composition: RewardCompositionSchema.nullable(),
|
|
242
|
+
leaseOwner: ReleaseIdSchema.nullable(),
|
|
243
|
+
leaseExpiresAt: ReleaseTimestampSchema.nullable(),
|
|
244
|
+
attemptCount: z.number().int().nonnegative(),
|
|
245
|
+
timeoutMs: z.number().int().min(100).max(300_000),
|
|
246
|
+
maximumSpendUsd: z.number().nonnegative().max(1_000),
|
|
247
|
+
failure: z.string().max(20_000).nullable(),
|
|
248
|
+
createdAt: ReleaseTimestampSchema,
|
|
249
|
+
updatedAt: ReleaseTimestampSchema,
|
|
250
|
+
}).strict();
|
|
251
|
+
export const LearningResourceSchema = z.union([TaskDefinitionSchema, RewardReleaseSchema, RewardBindingSchema, LearningSourceSchema, TaskEvidenceSchema, TaskFeedbackSchema, TaskAdmissionDecisionSchema, TaskBatchSchema, LearningPolicySchema, LearningIterationSchema, TaskGradeRunSchema]);
|
|
252
|
+
export function learningRef(resource) {
|
|
253
|
+
return LearningRevisionRefSchema.parse({ id: resource.id, revision: resource.revision, contentHash: resource.contentHash });
|
|
254
|
+
}
|
|
255
|
+
export function sameLearningRef(left, right) {
|
|
256
|
+
return left.id === right.id && left.revision === right.revision && left.contentHash === right.contentHash;
|
|
257
|
+
}
|
|
258
|
+
export function sealLearningContent(content) {
|
|
259
|
+
assertBoundedTaskJson(content, 16_777_216);
|
|
260
|
+
return { ...content, contentHash: contentHash(content) };
|
|
261
|
+
}
|
|
262
|
+
export function assertLearningContentHash(resource) {
|
|
263
|
+
const { contentHash: hash, ...content } = resource;
|
|
264
|
+
assertBoundedTaskJson(content, 16_777_216);
|
|
265
|
+
if (contentHash(content) !== hash)
|
|
266
|
+
throw new LearningDomainError("learning_content_hash_mismatch", 422);
|
|
267
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Stable domain failures that hosts may expose without treating rejected work
|
|
2
|
+
* as an infrastructure failure. Unexpected exceptions remain server errors. */
|
|
3
|
+
export class LearningDomainError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(code, status = 422, description) {
|
|
7
|
+
super(description ? `${code}: ${description}` : code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "LearningDomainError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { executeRewardBinding } from "../rewards.js";
|
|
2
|
+
import { assertGradeIdentity, taskAttemptEvidence, taskRecordFromEvidence } from "./admission.js";
|
|
3
|
+
import { TaskGradeRunSchema } from "./contracts.js";
|
|
4
|
+
import { requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
5
|
+
export function createBuiltinTaskGradeExecutor() {
|
|
6
|
+
return {
|
|
7
|
+
execute(input) {
|
|
8
|
+
return executeRewardBinding({ binding: input.binding, rewards: input.rewards, task: taskRecordFromEvidence(input.evidence, input.definition), evidence: taskAttemptEvidence(input.evidence, input.run.output), signal: input.signal });
|
|
9
|
+
},
|
|
10
|
+
// This executor allocates no remote compute and runs only bounded portable checks.
|
|
11
|
+
async cancel() { return true; },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createTaskGradeWorker(repository, executor, options) {
|
|
15
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
16
|
+
const active = new Map();
|
|
17
|
+
const key = (scope, id) => JSON.stringify([scope, id]);
|
|
18
|
+
async function run(scope, gradeId) {
|
|
19
|
+
const claim = await repository.transaction(scope, async (transaction) => {
|
|
20
|
+
const current = await requireLearningResource(transaction, "grade", gradeId);
|
|
21
|
+
if (["completed", "failed", "cancelled"].includes(current.status))
|
|
22
|
+
return { acquired: false, run: current };
|
|
23
|
+
if (current.leaseExpiresAt !== null && Date.parse(current.leaseExpiresAt) > Date.parse(now()))
|
|
24
|
+
return { acquired: false, run: current };
|
|
25
|
+
const run = TaskGradeRunSchema.parse({
|
|
26
|
+
...current, revision: current.revision + 1, status: current.status === "cancelling" ? "cancelling" : "running",
|
|
27
|
+
leaseOwner: options.workerId, leaseExpiresAt: new Date(Date.parse(now()) + current.timeoutMs + 30_000).toISOString(),
|
|
28
|
+
attemptCount: current.attemptCount + 1, updatedAt: now(),
|
|
29
|
+
});
|
|
30
|
+
await transaction.put("grade", run, current.revision, { parentId: current.evidence.id, status: run.status });
|
|
31
|
+
return { acquired: true, run };
|
|
32
|
+
});
|
|
33
|
+
if (!claim.acquired)
|
|
34
|
+
return claim.run;
|
|
35
|
+
if (claim.run.status === "cancelling") {
|
|
36
|
+
const terminal = await executor.cancel({ scope, run: claim.run });
|
|
37
|
+
return finish(scope, claim.run, terminal ? { status: "cancelled", composition: null, failure: null } : { status: "cancelling", composition: null, failure: null });
|
|
38
|
+
}
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
active.set(key(scope, gradeId), controller);
|
|
41
|
+
const timer = setTimeout(() => controller.abort(new Error("task_grade_timeout")), claim.run.timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const resolved = await repository.transaction(scope, async (transaction) => {
|
|
44
|
+
const evidence = await requireLearningRelease(transaction, "evidence", claim.run.evidence);
|
|
45
|
+
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
46
|
+
const binding = await requireLearningRelease(transaction, "binding", claim.run.binding);
|
|
47
|
+
const rewards = await Promise.all(binding.sources.map((source) => requireLearningRelease(transaction, "reward", source.reward)));
|
|
48
|
+
return { evidence, definition, binding, rewards };
|
|
49
|
+
});
|
|
50
|
+
controller.signal.throwIfAborted();
|
|
51
|
+
const composition = await executor.execute({ scope, run: claim.run, ...resolved, signal: controller.signal });
|
|
52
|
+
controller.signal.throwIfAborted();
|
|
53
|
+
assertGradeIdentity(composition, resolved.evidence, resolved.definition, claim.run.output);
|
|
54
|
+
return await finish(scope, claim.run, { status: "completed", composition, failure: null });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (controller.signal.aborted) {
|
|
58
|
+
const terminal = await executor.cancel({ scope, run: claim.run });
|
|
59
|
+
if (!terminal)
|
|
60
|
+
return finish(scope, claim.run, { status: "cancelling", composition: null, failure: "Execution cancellation is awaiting owner confirmation." });
|
|
61
|
+
if (controller.signal.reason instanceof Error && controller.signal.reason.message === "task_grade_cancel_requested")
|
|
62
|
+
return finish(scope, claim.run, { status: "cancelled", composition: null, failure: null });
|
|
63
|
+
}
|
|
64
|
+
return await finish(scope, claim.run, { status: "failed", composition: null, failure: error instanceof Error ? error.message : "Task grading failed." });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
active.delete(key(scope, gradeId));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function finish(scope, claimed, outcome) {
|
|
72
|
+
return repository.transaction(scope, async (transaction) => {
|
|
73
|
+
const current = await requireLearningResource(transaction, "grade", claimed.id);
|
|
74
|
+
if (current.leaseOwner !== options.workerId || current.attemptCount !== claimed.attemptCount || ["completed", "failed", "cancelled"].includes(current.status))
|
|
75
|
+
return current;
|
|
76
|
+
// A cancellation request does not override the execution owner's result.
|
|
77
|
+
const status = outcome.status;
|
|
78
|
+
const updated = TaskGradeRunSchema.parse({ ...current, ...outcome, status, composition: status === "completed" ? outcome.composition : null, revision: current.revision + 1, leaseOwner: null, leaseExpiresAt: status === "cancelling" ? new Date(Date.parse(now()) + 5_000).toISOString() : null, updatedAt: now() });
|
|
79
|
+
await transaction.put("grade", updated, current.revision, { parentId: current.evidence.id, status: updated.status });
|
|
80
|
+
return updated;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
run,
|
|
85
|
+
requestCancellation(scope, gradeId) { active.get(key(scope, gradeId))?.abort(new Error("task_grade_cancel_requested")); },
|
|
86
|
+
async drain(scope, limit = 20) {
|
|
87
|
+
const candidates = await repository.transaction(scope, async (transaction) => {
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (const status of ["queued", "running", "cancelling"])
|
|
90
|
+
rows.push(...(await transaction.list("grade", { status, limit: Math.max(1, Math.min(100, limit)) })).items);
|
|
91
|
+
return rows;
|
|
92
|
+
});
|
|
93
|
+
const completed = [];
|
|
94
|
+
for (const candidate of candidates)
|
|
95
|
+
completed.push(await run(scope, candidate.id));
|
|
96
|
+
return completed;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./assets.js";
|
|
3
|
+
export * from "./admission.js";
|
|
4
|
+
export * from "./operations.js";
|
|
5
|
+
export * from "./repository.js";
|
|
6
|
+
export * from "./service.js";
|
|
7
|
+
export * from "./grade-worker.js";
|
|
8
|
+
export * from "./transport.js";
|
|
9
|
+
export * from "./errors.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReleaseIdSchema } from "@openpond/harness";
|
|
3
|
+
import { LearningJsonObjectSchema, LearningRevisionRefSchema, LearningSourceContentSchema, TaskDefinitionContentSchema, TaskExampleSubmissionSchema, TaskFeedbackSubmissionSchema, LearningPolicyContentSchema } from "./contracts.js";
|
|
4
|
+
import { RewardBindingContentSchema, RewardReleaseContentSchema } from "../rewards.js";
|
|
5
|
+
import { LearningTextAssetContentSchema } from "./assets.js";
|
|
6
|
+
const command = z.object({ operationId: ReleaseIdSchema });
|
|
7
|
+
const publish = command.extend({ action: z.literal("publish"), expectedRevision: z.number().int().nonnegative() });
|
|
8
|
+
export const PublishLearningResourceCommandSchema = z.discriminatedUnion("kind", [
|
|
9
|
+
publish.extend({ kind: z.literal("asset"), content: LearningTextAssetContentSchema }).strict(),
|
|
10
|
+
publish.extend({ kind: z.literal("definition"), content: TaskDefinitionContentSchema }).strict(),
|
|
11
|
+
publish.extend({ kind: z.literal("reward"), content: RewardReleaseContentSchema }).strict(),
|
|
12
|
+
publish.extend({ kind: z.literal("binding"), content: RewardBindingContentSchema }).strict(),
|
|
13
|
+
publish.extend({ kind: z.literal("source"), content: LearningSourceContentSchema }).strict(),
|
|
14
|
+
publish.extend({ kind: z.literal("policy"), content: LearningPolicyContentSchema }).strict(),
|
|
15
|
+
]);
|
|
16
|
+
export const PublishLearningResourcesCommandSchema = command.extend({
|
|
17
|
+
action: z.literal("publish_resources"),
|
|
18
|
+
resources: z.array(z.union(PublishLearningResourceCommandSchema.options.map((schema) => schema.omit({ operationId: true, action: true })))).min(1).max(20),
|
|
19
|
+
}).strict();
|
|
20
|
+
export const SubmitTaskExampleCommandSchema = command.extend({ action: z.literal("submit_example"), example: TaskExampleSubmissionSchema }).strict();
|
|
21
|
+
export const SubmitTaskFeedbackCommandSchema = command.extend({ action: z.literal("submit_feedback"), feedback: TaskFeedbackSubmissionSchema }).strict();
|
|
22
|
+
export const ApplyTaskCorrectionCommandSchema = command.extend({ action: z.literal("apply_correction"), feedbackId: ReleaseIdSchema, evidence: LearningRevisionRefSchema }).strict();
|
|
23
|
+
export const ResolveTaskFeedbackCommandSchema = command.extend({ action: z.literal("resolve_feedback"), feedbackId: ReleaseIdSchema, expectedRevision: z.number().int().positive(), disposition: z.enum(["applied", "rejected", "superseded"]), decision: LearningRevisionRefSchema.nullable(), note: z.string().max(20_000) }).strict();
|
|
24
|
+
export const QueueTaskGradeCommandSchema = command.extend({ action: z.literal("queue_grade"), evidence: LearningRevisionRefSchema, target: z.enum(["observed", "proposed_target"]), proposedTarget: LearningJsonObjectSchema.nullable(), timeoutMs: z.number().int().min(100).max(300_000).default(30_000), maximumSpendUsd: z.number().nonnegative().max(1_000).default(0) }).strict();
|
|
25
|
+
export const ReviewTaskEvidenceCommandSchema = command.extend({
|
|
26
|
+
action: z.literal("review"), evidence: LearningRevisionRefSchema, expectedRevision: z.number().int().nonnegative(),
|
|
27
|
+
disposition: z.enum(["approved", "rejected", "pending"]), targetApproval: z.enum(["approved", "rejected", "pending", "not_required"]),
|
|
28
|
+
approvedTarget: LearningJsonObjectSchema.nullable(), observedGradeId: ReleaseIdSchema.nullable(), targetGradeId: ReleaseIdSchema.nullable(),
|
|
29
|
+
note: z.string().max(20_000),
|
|
30
|
+
}).strict();
|
|
31
|
+
export const SealTaskBatchCommandSchema = command.extend({ action: z.literal("seal_batch"), batchId: ReleaseIdSchema, taskDefinition: LearningRevisionRefSchema, purpose: z.enum(["supervised_training", "reward_training", "evaluation"]), evidence: z.array(LearningRevisionRefSchema).min(1).max(10_000), decisions: z.array(LearningRevisionRefSchema).min(1).max(10_000) }).strict();
|
|
32
|
+
export const CancelTaskGradeCommandSchema = command.extend({ action: z.literal("cancel_grade"), gradeId: ReleaseIdSchema, expectedRevision: z.number().int().positive() }).strict();
|
|
33
|
+
export const LearningCommandSchema = z.union([PublishLearningResourceCommandSchema, PublishLearningResourcesCommandSchema, SubmitTaskExampleCommandSchema, SubmitTaskFeedbackCommandSchema, ApplyTaskCorrectionCommandSchema, ResolveTaskFeedbackCommandSchema, QueueTaskGradeCommandSchema, ReviewTaskEvidenceCommandSchema, SealTaskBatchCommandSchema, CancelTaskGradeCommandSchema]);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { contentHash } from "@openpond/harness";
|
|
3
|
+
import { assertLearningContentHash, LearningSourceSchema, TaskAdmissionDecisionSchema, TaskBatchSchema, TaskDefinitionSchema, TaskEvidenceSchema, TaskFeedbackSchema, TaskGradeRunSchema, LearningPolicySchema, LearningIterationSchema } from "./contracts.js";
|
|
4
|
+
import { RewardBindingSchema, RewardReleaseSchema } from "../rewards.js";
|
|
5
|
+
import { TasksetReleaseSchema } from "../tasksets.js";
|
|
6
|
+
import { LearningDomainError } from "./errors.js";
|
|
7
|
+
import { LearningTextAssetSchema } from "./assets.js";
|
|
8
|
+
export const learningResourceSchemas = {
|
|
9
|
+
asset: LearningTextAssetSchema,
|
|
10
|
+
definition: TaskDefinitionSchema, reward: RewardReleaseSchema, binding: RewardBindingSchema,
|
|
11
|
+
source: LearningSourceSchema, evidence: TaskEvidenceSchema, feedback: TaskFeedbackSchema,
|
|
12
|
+
decision: TaskAdmissionDecisionSchema, batch: TaskBatchSchema, policy: LearningPolicySchema,
|
|
13
|
+
iteration: LearningIterationSchema, grade: TaskGradeRunSchema, package: TasksetReleaseSchema,
|
|
14
|
+
};
|
|
15
|
+
export const LearningResourceKindSchema = z.enum(Object.keys(learningResourceSchemas));
|
|
16
|
+
export class LearningConflictError extends LearningDomainError {
|
|
17
|
+
kind;
|
|
18
|
+
id;
|
|
19
|
+
expectedRevision;
|
|
20
|
+
currentRevision;
|
|
21
|
+
constructor(kind, id, expectedRevision, currentRevision) {
|
|
22
|
+
super("learning_revision_conflict", 409, `Revision conflict for ${kind} ${id}: expected ${expectedRevision}, current ${currentRevision}.`);
|
|
23
|
+
this.kind = kind;
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.expectedRevision = expectedRevision;
|
|
26
|
+
this.currentRevision = currentRevision;
|
|
27
|
+
this.name = "LearningConflictError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function requireLearningResource(transaction, kind, id, revision) {
|
|
31
|
+
const resource = await transaction.get(kind, id, revision);
|
|
32
|
+
if (!resource)
|
|
33
|
+
throw new LearningDomainError("learning_resource_not_found", 404, `${kind}:${id}`);
|
|
34
|
+
return learningResourceSchemas[kind].parse(resource);
|
|
35
|
+
}
|
|
36
|
+
export async function requireLearningRelease(transaction, kind, ref) {
|
|
37
|
+
const resource = await requireLearningResource(transaction, kind, ref.id, ref.revision);
|
|
38
|
+
if (!("contentHash" in resource) || resource.contentHash !== ref.contentHash)
|
|
39
|
+
throw new LearningDomainError("learning_release_hash_mismatch", 409, `${kind}:${ref.id}`);
|
|
40
|
+
assertLearningContentHash(resource);
|
|
41
|
+
return resource;
|
|
42
|
+
}
|
|
43
|
+
export function learningEvidenceId(sourceId, exampleId, attemptId) {
|
|
44
|
+
return `evidence-${contentHash([sourceId, exampleId, attemptId])}`;
|
|
45
|
+
}
|
|
46
|
+
export function learningOperationId(actorId, operationId) {
|
|
47
|
+
return contentHash([actorId, operationId]);
|
|
48
|
+
}
|