@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,367 @@
|
|
|
1
|
+
import { LearningDomainError } from "./errors.js";
|
|
2
|
+
import { contentHash } from "@openpond/harness";
|
|
3
|
+
import { createRewardBinding, createRewardRelease, resolveBoundRewards } from "../rewards.js";
|
|
4
|
+
import { assertBoundedTaskJson, validateTaskValue } from "../task-schema.js";
|
|
5
|
+
import { assertAdmissionDecision, assertGradeIdentity, compileTaskBatch, inspectTaskEvidence, sealTaskBatch, taskFamilyReservations } from "./admission.js";
|
|
6
|
+
import { learningRef, LearningSourceSchema, LearningPolicySchema, sameLearningRef, sealLearningContent, TaskAdmissionDecisionContentSchema, TaskAdmissionDecisionSchema, TaskDefinitionSchema, TaskEvidenceContentSchema, TaskEvidenceSchema, TaskFeedbackSchema, TaskGradeRunSchema } from "./contracts.js";
|
|
7
|
+
import { LearningCommandSchema } from "./operations.js";
|
|
8
|
+
import { LearningConflictError, learningEvidenceId, learningOperationId, requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
9
|
+
import { validateSourceSubmission } from "./admission.js";
|
|
10
|
+
import { LearningTextAssetSchema, verifyLearningTextAsset } from "./assets.js";
|
|
11
|
+
export function createLearningService(repository, options = {}) {
|
|
12
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
13
|
+
async function command(context, raw) {
|
|
14
|
+
assertBoundedTaskJson(raw, 16_777_216);
|
|
15
|
+
const input = LearningCommandSchema.parse(raw);
|
|
16
|
+
authorize(context, input);
|
|
17
|
+
const producer = input.action === "submit_example" ? input.example : input.action === "submit_feedback" ? input.feedback : null;
|
|
18
|
+
const operationId = producer ? contentHash([input.action, producer.sourceId, producer.idempotencyKey]) : learningOperationId(context.actor.id, input.operationId);
|
|
19
|
+
const requestHash = contentHash(producer ? { action: input.action, value: producer } : input);
|
|
20
|
+
return repository.transaction(context.scope, async (transaction) => {
|
|
21
|
+
const previous = await transaction.operation(operationId);
|
|
22
|
+
if (previous) {
|
|
23
|
+
if (previous.requestHash !== requestHash)
|
|
24
|
+
throw new LearningDomainError("learning_idempotency_conflict", 409);
|
|
25
|
+
return { operationId: input.operationId, resources: await Promise.all(previous.resources.map((pointer) => requireLearningResource(transaction, pointer.kind, pointer.id, pointer.revision))) };
|
|
26
|
+
}
|
|
27
|
+
let pointers;
|
|
28
|
+
switch (input.action) {
|
|
29
|
+
case "publish":
|
|
30
|
+
pointers = [await publish(transaction, input)];
|
|
31
|
+
break;
|
|
32
|
+
case "publish_resources": {
|
|
33
|
+
pointers = [];
|
|
34
|
+
for (const resource of input.resources)
|
|
35
|
+
pointers.push(await publish(transaction, { ...resource, action: "publish", operationId: input.operationId }));
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "submit_example":
|
|
39
|
+
pointers = [await submit(transaction, input)];
|
|
40
|
+
break;
|
|
41
|
+
case "submit_feedback":
|
|
42
|
+
pointers = [await feedback(transaction, input)];
|
|
43
|
+
break;
|
|
44
|
+
case "apply_correction":
|
|
45
|
+
pointers = await correct(transaction, input, context.actor.id);
|
|
46
|
+
break;
|
|
47
|
+
case "resolve_feedback":
|
|
48
|
+
pointers = [await resolveFeedback(transaction, input, context.actor.id)];
|
|
49
|
+
break;
|
|
50
|
+
case "queue_grade":
|
|
51
|
+
pointers = [await queueGrade(transaction, input, operationId)];
|
|
52
|
+
break;
|
|
53
|
+
case "cancel_grade":
|
|
54
|
+
pointers = [await cancelGrade(transaction, input)];
|
|
55
|
+
break;
|
|
56
|
+
case "review":
|
|
57
|
+
pointers = [await review(transaction, input, context.actor.id)];
|
|
58
|
+
break;
|
|
59
|
+
case "seal_batch":
|
|
60
|
+
pointers = await seal(transaction, input, context.actor.id);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
await transaction.saveOperation(operationId, { requestHash, resources: pointers });
|
|
64
|
+
return { operationId: input.operationId, resources: await Promise.all(pointers.map((pointer) => requireLearningResource(transaction, pointer.kind, pointer.id, pointer.revision))) };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function publish(transaction, input) {
|
|
68
|
+
if (input.content.revision !== input.expectedRevision + 1)
|
|
69
|
+
throw new LearningDomainError("learning_publication_revision_invalid", 422);
|
|
70
|
+
let resource;
|
|
71
|
+
switch (input.kind) {
|
|
72
|
+
case "asset": {
|
|
73
|
+
resource = LearningTextAssetSchema.parse(sealLearningContent(input.content));
|
|
74
|
+
const existing = await transaction.get("asset", resource.id, 1);
|
|
75
|
+
if (existing?.contentHash === resource.contentHash)
|
|
76
|
+
return pointer("asset", existing);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "reward": {
|
|
80
|
+
resource = createRewardRelease(input.content);
|
|
81
|
+
const implementation = input.content.implementation;
|
|
82
|
+
const references = [
|
|
83
|
+
...input.content.assets,
|
|
84
|
+
...("verifierRef" in implementation ? [implementation.verifierRef] : []),
|
|
85
|
+
...("rubricRef" in implementation ? [implementation.rubricRef] : []),
|
|
86
|
+
...("inputContract" in implementation ? [implementation.inputContract] : []),
|
|
87
|
+
];
|
|
88
|
+
for (const reference of references) {
|
|
89
|
+
const asset = await requireLearningResource(transaction, "asset", reference.id, 1);
|
|
90
|
+
verifyLearningTextAsset(asset, reference);
|
|
91
|
+
if (reference.visibility === "policy")
|
|
92
|
+
throw new LearningDomainError("reward_asset_visibility_invalid", 422, "Reward source and rubrics must be private to the evaluator.");
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "binding": {
|
|
97
|
+
if (input.content.recipeRef)
|
|
98
|
+
await requireLearningRelease(transaction, "binding", input.content.recipeRef);
|
|
99
|
+
const rewards = await Promise.all(input.content.sources.map((source) => requireLearningRelease(transaction, "reward", source.reward)));
|
|
100
|
+
resource = createRewardBinding(input.content, rewards);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "definition": {
|
|
104
|
+
await requireLearningRelease(transaction, "binding", input.content.rewardBinding);
|
|
105
|
+
resource = TaskDefinitionSchema.parse(sealLearningContent(input.content));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "source": {
|
|
109
|
+
await requireLearningRelease(transaction, "definition", input.content.taskDefinition);
|
|
110
|
+
resource = LearningSourceSchema.parse(sealLearningContent(input.content));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "policy": {
|
|
114
|
+
const definition = await requireLearningRelease(transaction, "definition", input.content.taskDefinition);
|
|
115
|
+
if (!sameLearningRef(definition.rewardBinding, input.content.rewardBinding))
|
|
116
|
+
throw new LearningDomainError("learning_policy_binding_mismatch", 422);
|
|
117
|
+
for (const source of input.content.sources) {
|
|
118
|
+
const resource = await requireLearningRelease(transaction, "source", source);
|
|
119
|
+
if (!sameLearningRef(resource.taskDefinition, input.content.taskDefinition))
|
|
120
|
+
throw new LearningDomainError("learning_policy_source_definition_mismatch", 422);
|
|
121
|
+
}
|
|
122
|
+
resource = LearningPolicySchema.parse(sealLearningContent(input.content));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await transaction.put(input.kind, resource, input.expectedRevision);
|
|
127
|
+
return pointer(input.kind, resource);
|
|
128
|
+
}
|
|
129
|
+
async function submit(transaction, input) {
|
|
130
|
+
const source = await requireLearningResource(transaction, "source", input.example.sourceId);
|
|
131
|
+
const submission = validateSourceSubmission(source, input.example);
|
|
132
|
+
await requireLearningRelease(transaction, "definition", submission.taskDefinition);
|
|
133
|
+
const id = learningEvidenceId(submission.sourceId, submission.exampleId, submission.attemptId);
|
|
134
|
+
const existing = await transaction.get("evidence", id, 1);
|
|
135
|
+
if (existing) {
|
|
136
|
+
const { idempotencyKey: _oldKey, ...oldPayload } = existing.submission;
|
|
137
|
+
const { idempotencyKey: _newKey, ...newPayload } = submission;
|
|
138
|
+
if (contentHash(oldPayload) !== contentHash(newPayload))
|
|
139
|
+
throw new LearningDomainError("task_evidence_identity_conflict", 409);
|
|
140
|
+
return pointer("evidence", existing);
|
|
141
|
+
}
|
|
142
|
+
const evidence = TaskEvidenceSchema.parse(sealLearningContent(TaskEvidenceContentSchema.parse({
|
|
143
|
+
schemaVersion: "openpond.taskEvidence.v1", id, revision: 1, source: learningRef(source), submission,
|
|
144
|
+
supersedes: null, correctionFeedbackId: null, receivedAt: now(),
|
|
145
|
+
})));
|
|
146
|
+
await transaction.put("evidence", evidence, 0, { parentId: source.id });
|
|
147
|
+
let afterId;
|
|
148
|
+
do {
|
|
149
|
+
const page = await transaction.list("feedback", { parentId: evidence.id, status: "pending_example", limit: 100, ...(afterId ? { afterId } : {}) });
|
|
150
|
+
for (const feedback of page.items) {
|
|
151
|
+
const updated = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: "pending_review", evidence: learningRef(evidence) });
|
|
152
|
+
await transaction.put("feedback", updated, feedback.revision, { parentId: evidence.id, status: updated.status });
|
|
153
|
+
}
|
|
154
|
+
afterId = page.nextCursor ?? undefined;
|
|
155
|
+
} while (afterId);
|
|
156
|
+
return pointer("evidence", evidence);
|
|
157
|
+
}
|
|
158
|
+
async function feedback(transaction, input) {
|
|
159
|
+
const source = await requireLearningResource(transaction, "source", input.feedback.sourceId);
|
|
160
|
+
if (!source.enabled)
|
|
161
|
+
throw new LearningDomainError("learning_source_disabled", 409);
|
|
162
|
+
const evidenceId = learningEvidenceId(source.id, input.feedback.exampleId, input.feedback.attemptId);
|
|
163
|
+
const evidence = await transaction.get("evidence", evidenceId);
|
|
164
|
+
const record = TaskFeedbackSchema.parse({
|
|
165
|
+
schemaVersion: "openpond.taskFeedbackRecord.v1", id: `feedback-${contentHash([source.id, input.feedback.idempotencyKey])}`,
|
|
166
|
+
submission: input.feedback, status: evidence ? "pending_review" : "pending_example",
|
|
167
|
+
evidence: evidence ? learningRef(evidence) : null, createdAt: now(), revision: 1,
|
|
168
|
+
});
|
|
169
|
+
await transaction.put("feedback", record, 0, { parentId: evidenceId, status: record.status });
|
|
170
|
+
return pointer("feedback", record);
|
|
171
|
+
}
|
|
172
|
+
async function correct(transaction, input, actorId) {
|
|
173
|
+
const feedback = await requireLearningResource(transaction, "feedback", input.feedbackId);
|
|
174
|
+
if (["applied", "rejected", "superseded"].includes(feedback.status))
|
|
175
|
+
throw new LearningDomainError("task_feedback_already_resolved", 409);
|
|
176
|
+
const evidence = await currentEvidence(transaction, input.evidence);
|
|
177
|
+
const expectedId = learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId);
|
|
178
|
+
if (expectedId !== evidence.id || (feedback.submission.expectedEvidenceHash !== null && feedback.submission.expectedEvidenceHash !== evidence.contentHash))
|
|
179
|
+
throw new LearningDomainError("task_feedback_evidence_mismatch", 409);
|
|
180
|
+
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
181
|
+
const submission = { ...evidence.submission };
|
|
182
|
+
const { kind, value } = feedback.submission;
|
|
183
|
+
if (kind === "ground_truth_correction") {
|
|
184
|
+
if (!validateTaskValue(definition.outputSchema, value).valid)
|
|
185
|
+
throw new LearningDomainError("task_corrected_expected_schema_invalid", 422);
|
|
186
|
+
submission.expected = value;
|
|
187
|
+
}
|
|
188
|
+
else if (kind === "input_correction") {
|
|
189
|
+
if (!validateTaskValue(definition.inputSchema, value).valid)
|
|
190
|
+
throw new LearningDomainError("task_corrected_input_schema_invalid", 422);
|
|
191
|
+
submission.input = value;
|
|
192
|
+
}
|
|
193
|
+
else if (kind === "family_resolution") {
|
|
194
|
+
if (typeof value.familyKey !== "string" || !value.familyKey.trim())
|
|
195
|
+
throw new LearningDomainError("task_family_resolution_invalid", 422);
|
|
196
|
+
submission.familyKey = value.familyKey;
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
throw new LearningDomainError("task_feedback_requires_target_review", 422);
|
|
200
|
+
const { contentHash: _priorHash, ...priorContent } = evidence;
|
|
201
|
+
const revised = TaskEvidenceSchema.parse(sealLearningContent(TaskEvidenceContentSchema.parse({
|
|
202
|
+
...priorContent, revision: evidence.revision + 1, submission, supersedes: learningRef(evidence), correctionFeedbackId: feedback.id, receivedAt: now(),
|
|
203
|
+
})));
|
|
204
|
+
const resolved = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: "applied", evidence: learningRef(revised), review: { actorId, decision: null, note: "Applied as a corrected evidence revision.", resolvedAt: now() } });
|
|
205
|
+
await transaction.put("evidence", revised, evidence.revision, { parentId: submission.sourceId });
|
|
206
|
+
await transaction.put("feedback", resolved, feedback.revision, { parentId: evidence.id, status: resolved.status });
|
|
207
|
+
return [pointer("evidence", revised), pointer("feedback", resolved)];
|
|
208
|
+
}
|
|
209
|
+
async function resolveFeedback(transaction, input, actorId) {
|
|
210
|
+
const feedback = await requireLearningResource(transaction, "feedback", input.feedbackId);
|
|
211
|
+
if (feedback.revision !== input.expectedRevision)
|
|
212
|
+
throw new LearningConflictError("feedback", feedback.id, input.expectedRevision, feedback.revision);
|
|
213
|
+
if (["applied", "rejected", "superseded"].includes(feedback.status))
|
|
214
|
+
throw new LearningDomainError("task_feedback_already_resolved", 409);
|
|
215
|
+
if (input.disposition === "applied" && !input.decision)
|
|
216
|
+
throw new LearningDomainError("task_feedback_decision_required");
|
|
217
|
+
if (input.decision) {
|
|
218
|
+
const decision = await requireLearningRelease(transaction, "decision", input.decision);
|
|
219
|
+
const current = await requireLearningResource(transaction, "decision", decision.id);
|
|
220
|
+
if (!sameLearningRef(learningRef(current), input.decision))
|
|
221
|
+
throw new LearningDomainError("task_admission_revision_stale", 409);
|
|
222
|
+
const evidence = await currentEvidence(transaction, decision.evidence);
|
|
223
|
+
if (learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId) !== evidence.id || (feedback.submission.expectedEvidenceHash !== null && feedback.submission.expectedEvidenceHash !== evidence.contentHash))
|
|
224
|
+
throw new LearningDomainError("task_feedback_evidence_mismatch", 409);
|
|
225
|
+
if (input.disposition === "applied") {
|
|
226
|
+
if (decision.taskAdmissibility === "pending")
|
|
227
|
+
throw new LearningDomainError("task_feedback_decision_pending");
|
|
228
|
+
if (feedback.submission.kind === "target_correction") {
|
|
229
|
+
if (decision.targetApproval !== "approved" || contentHash(decision.approvedTarget) !== contentHash(feedback.submission.value))
|
|
230
|
+
throw new LearningDomainError("task_feedback_target_not_approved");
|
|
231
|
+
}
|
|
232
|
+
else if (feedback.submission.kind !== "outcome")
|
|
233
|
+
throw new LearningDomainError("task_feedback_requires_evidence_correction");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const resolved = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: input.disposition, review: { actorId, decision: input.decision, note: input.note, resolvedAt: now() } });
|
|
237
|
+
await transaction.put("feedback", resolved, feedback.revision, { parentId: learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId), status: resolved.status });
|
|
238
|
+
return pointer("feedback", resolved);
|
|
239
|
+
}
|
|
240
|
+
async function queueGrade(transaction, input, operationId) {
|
|
241
|
+
const evidence = await currentEvidence(transaction, input.evidence);
|
|
242
|
+
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
243
|
+
if (!inspectTaskEvidence(evidence, definition).taskReady)
|
|
244
|
+
throw new LearningDomainError("task_evidence_not_ready", 422);
|
|
245
|
+
const output = input.target === "observed" ? evidence.submission.observedOutput : input.proposedTarget;
|
|
246
|
+
if (output === null)
|
|
247
|
+
throw new LearningDomainError("task_grade_output_required", 422);
|
|
248
|
+
if (input.target === "observed" && input.proposedTarget !== null)
|
|
249
|
+
throw new LearningDomainError("observed_grade_cannot_replace_output", 422);
|
|
250
|
+
const grade = TaskGradeRunSchema.parse({
|
|
251
|
+
schemaVersion: "openpond.taskGradeRun.v1", id: `grade-${operationId}`, revision: 1,
|
|
252
|
+
evidence: learningRef(evidence), binding: definition.rewardBinding, target: input.target, output,
|
|
253
|
+
status: "queued", composition: null, leaseOwner: null, leaseExpiresAt: null, attemptCount: 0,
|
|
254
|
+
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, failure: null, createdAt: now(), updatedAt: now(),
|
|
255
|
+
});
|
|
256
|
+
await transaction.put("grade", grade, 0, { parentId: evidence.id, status: grade.status });
|
|
257
|
+
return pointer("grade", grade);
|
|
258
|
+
}
|
|
259
|
+
async function cancelGrade(transaction, input) {
|
|
260
|
+
const grade = await requireLearningResource(transaction, "grade", input.gradeId);
|
|
261
|
+
if (grade.revision !== input.expectedRevision)
|
|
262
|
+
throw new LearningConflictError("grade", grade.id, input.expectedRevision, grade.revision);
|
|
263
|
+
if (["completed", "failed", "cancelled"].includes(grade.status))
|
|
264
|
+
return pointer("grade", grade);
|
|
265
|
+
const updated = TaskGradeRunSchema.parse({ ...grade, revision: grade.revision + 1, status: grade.status === "queued" ? "cancelled" : "cancelling", updatedAt: now() });
|
|
266
|
+
await transaction.put("grade", updated, grade.revision, { parentId: grade.evidence.id, status: updated.status });
|
|
267
|
+
return pointer("grade", updated);
|
|
268
|
+
}
|
|
269
|
+
async function review(transaction, input, actorId) {
|
|
270
|
+
const evidence = await currentEvidence(transaction, input.evidence);
|
|
271
|
+
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
272
|
+
const inspection = inspectTaskEvidence(evidence, definition);
|
|
273
|
+
const id = `decision-${evidence.id}`;
|
|
274
|
+
const previous = await transaction.get("decision", id);
|
|
275
|
+
const observedGrade = await gradeResult(transaction, input.observedGradeId, evidence, "observed");
|
|
276
|
+
const targetGrade = await gradeResult(transaction, input.targetGradeId, evidence, "proposed_target");
|
|
277
|
+
const measured = observedGrade?.training.status === "scored" ? observedGrade.training : observedGrade?.evaluation;
|
|
278
|
+
const decision = TaskAdmissionDecisionSchema.parse(sealLearningContent(TaskAdmissionDecisionContentSchema.parse({
|
|
279
|
+
schemaVersion: "openpond.taskAdmissionDecision.v1", id, revision: input.expectedRevision + 1,
|
|
280
|
+
evidence: learningRef(evidence), supersedes: previous ? learningRef(previous) : null,
|
|
281
|
+
actor: { kind: "human", id: actorId, policy: null }, evidenceValidity: inspection.evidenceValidity,
|
|
282
|
+
taskAdmissibility: input.disposition, observedQuality: measured?.status === "scored" ? measured.passed ? "passed" : "failed" : observedGrade ? "unavailable" : "unscored",
|
|
283
|
+
targetApproval: input.targetApproval, approvedTarget: input.approvedTarget, grade: observedGrade, targetGrade,
|
|
284
|
+
note: input.note, decidedAt: now(),
|
|
285
|
+
})));
|
|
286
|
+
if (input.disposition === "approved")
|
|
287
|
+
assertAdmissionDecision({ decision, evidence, definition, purpose: input.targetApproval === "approved" ? "supervised_training" : "reward_training" });
|
|
288
|
+
await transaction.put("decision", decision, input.expectedRevision, { parentId: evidence.id, status: decision.taskAdmissibility });
|
|
289
|
+
return pointer("decision", decision);
|
|
290
|
+
}
|
|
291
|
+
async function seal(transaction, input, actorId) {
|
|
292
|
+
const definition = await requireLearningRelease(transaction, "definition", input.taskDefinition);
|
|
293
|
+
const binding = await requireLearningRelease(transaction, "binding", definition.rewardBinding);
|
|
294
|
+
const rewards = await Promise.all(binding.sources.map((source) => requireLearningRelease(transaction, "reward", source.reward)));
|
|
295
|
+
resolveBoundRewards(binding, rewards);
|
|
296
|
+
const evidence = await Promise.all(input.evidence.map((ref) => currentEvidence(transaction, ref)));
|
|
297
|
+
const decisions = await Promise.all(input.decisions.map(async (ref) => {
|
|
298
|
+
const decision = await requireLearningRelease(transaction, "decision", ref);
|
|
299
|
+
const current = await requireLearningResource(transaction, "decision", ref.id);
|
|
300
|
+
if (!sameLearningRef(learningRef(current), ref))
|
|
301
|
+
throw new LearningDomainError("task_admission_revision_stale", 409);
|
|
302
|
+
return decision;
|
|
303
|
+
}));
|
|
304
|
+
const priorSplits = [];
|
|
305
|
+
for (const item of evidence) {
|
|
306
|
+
for (const reservation of taskFamilyReservations(item, definition)) {
|
|
307
|
+
const split = await transaction.familySplit(reservation.namespace, reservation.kind, reservation.key);
|
|
308
|
+
if (split !== null)
|
|
309
|
+
priorSplits.push({ ...reservation, split: split });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const batch = sealTaskBatch({ id: input.batchId, definition, binding, rewards, purpose: input.purpose, evidence, decisions, priorSplits, actorId, now: now() });
|
|
313
|
+
const release = compileTaskBatch({ batch, definition, binding, rewards, evidence, decisions });
|
|
314
|
+
for (const item of evidence) {
|
|
315
|
+
for (const reservation of taskFamilyReservations(item, definition))
|
|
316
|
+
await transaction.reserveFamilySplit(reservation.namespace, reservation.kind, reservation.key, reservation.split);
|
|
317
|
+
}
|
|
318
|
+
await transaction.put("batch", batch, 0, { parentId: definition.id });
|
|
319
|
+
await transaction.put("package", release, 0, { parentId: batch.id });
|
|
320
|
+
return [pointer("batch", batch), pointer("package", release)];
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
command,
|
|
324
|
+
async get(context, kind, id, revision) {
|
|
325
|
+
authorizeRead(context);
|
|
326
|
+
return repository.transaction(context.scope, (transaction) => requireLearningResource(transaction, kind, id, revision));
|
|
327
|
+
},
|
|
328
|
+
async list(context, kind, query = {}) {
|
|
329
|
+
authorizeRead(context);
|
|
330
|
+
const limit = Math.max(1, Math.min(100, Math.trunc(query.limit ?? 50)));
|
|
331
|
+
return repository.transaction(context.scope, (transaction) => transaction.list(kind, { ...query, limit }));
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function currentEvidence(transaction, ref) {
|
|
336
|
+
const evidence = await requireLearningRelease(transaction, "evidence", ref);
|
|
337
|
+
const current = await requireLearningResource(transaction, "evidence", ref.id);
|
|
338
|
+
if (!sameLearningRef(learningRef(current), ref))
|
|
339
|
+
throw new LearningDomainError("task_evidence_revision_stale", 409);
|
|
340
|
+
return evidence;
|
|
341
|
+
}
|
|
342
|
+
async function gradeResult(transaction, id, evidence, target) {
|
|
343
|
+
if (id === null)
|
|
344
|
+
return null;
|
|
345
|
+
const grade = await requireLearningResource(transaction, "grade", id);
|
|
346
|
+
if (grade.status !== "completed" || !grade.composition || grade.target !== target || !sameLearningRef(grade.evidence, learningRef(evidence)))
|
|
347
|
+
throw new LearningDomainError("task_grade_not_applicable", 422);
|
|
348
|
+
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
349
|
+
assertGradeIdentity(grade.composition, evidence, definition, grade.output);
|
|
350
|
+
return grade.composition;
|
|
351
|
+
}
|
|
352
|
+
function pointer(kind, resource) { return { kind, id: resource.id, revision: resource.revision }; }
|
|
353
|
+
function authorizeRead(context) {
|
|
354
|
+
if (!context.scope.trim() || !context.actor.id.trim() || context.actor.role === "source")
|
|
355
|
+
throw new LearningDomainError("learning_read_not_authorized", 403);
|
|
356
|
+
}
|
|
357
|
+
function authorize(context, input) {
|
|
358
|
+
if (!context.scope.trim() || !context.actor.id.trim())
|
|
359
|
+
throw new LearningDomainError("learning_scope_required", 400);
|
|
360
|
+
if (context.actor.role === "source") {
|
|
361
|
+
const sourceId = input.action === "submit_example" ? input.example.sourceId : input.action === "submit_feedback" ? input.feedback.sourceId : null;
|
|
362
|
+
if (!sourceId || sourceId !== context.actor.sourceId)
|
|
363
|
+
throw new LearningDomainError("learning_source_not_authorized", 403);
|
|
364
|
+
}
|
|
365
|
+
if (["review", "apply_correction", "resolve_feedback", "seal_batch"].includes(input.action) && context.actor.role !== "reviewer")
|
|
366
|
+
throw new LearningDomainError("learning_review_not_authorized", 403);
|
|
367
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReleaseIdSchema } from "@openpond/harness";
|
|
3
|
+
import { LearningCommandSchema } from "./operations.js";
|
|
4
|
+
import { LearningResourceKindSchema, learningResourceSchemas } from "./repository.js";
|
|
5
|
+
import { assertBoundedTaskJson } from "../task-schema.js";
|
|
6
|
+
import { LearningDomainError } from "./errors.js";
|
|
7
|
+
/** Run before recursive schema parsing, including for non-TypeScript producers. */
|
|
8
|
+
export function assertLearningRequestJson(raw) {
|
|
9
|
+
try {
|
|
10
|
+
assertBoundedTaskJson(raw, 16_777_216);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new LearningDomainError("learning_json_invalid", 400, error instanceof Error ? error.message : "Expected bounded JSON.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Scope selection is never authorization; each host resolves its authenticated owner. */
|
|
17
|
+
export const LearningCommandRequestSchema = z.object({ scope: ReleaseIdSchema, command: LearningCommandSchema }).strict();
|
|
18
|
+
export const LearningReadRequestSchema = z.discriminatedUnion("action", [
|
|
19
|
+
z.object({ action: z.literal("get"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, id: ReleaseIdSchema, revision: z.number().int().positive().optional() }).strict(),
|
|
20
|
+
z.object({ action: z.literal("list"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, parentId: ReleaseIdSchema.optional(), status: z.string().min(1).max(200).optional(), afterId: ReleaseIdSchema.optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(),
|
|
21
|
+
]);
|
|
22
|
+
export const LearningOperationResultSchema = z.object({
|
|
23
|
+
operationId: ReleaseIdSchema,
|
|
24
|
+
resources: z.array(z.union(Object.values(learningResourceSchemas))).max(100),
|
|
25
|
+
}).strict();
|
package/dist/preferences.js
CHANGED
|
@@ -13,6 +13,11 @@ export const PreferenceReviewerKindSchema = z.enum([
|
|
|
13
13
|
"human",
|
|
14
14
|
"model",
|
|
15
15
|
"deterministic",
|
|
16
|
+
"fixture",
|
|
17
|
+
]);
|
|
18
|
+
export const PreferenceRewardScopeSchema = z.enum([
|
|
19
|
+
"production",
|
|
20
|
+
"synthetic_smoke",
|
|
16
21
|
]);
|
|
17
22
|
export const PreferenceRendererSchema = z.enum([
|
|
18
23
|
"markdown",
|
|
@@ -111,7 +116,7 @@ const ComparisonCandidateSchema = z.object({
|
|
|
111
116
|
attemptRef: ImmutableReleaseRefSchema,
|
|
112
117
|
runManifestRef: ImmutableReleaseRefSchema,
|
|
113
118
|
artifactManifestRef: ImmutableReleaseRefSchema,
|
|
114
|
-
visibleArtifactIds: z.array(ReleaseIdSchema).
|
|
119
|
+
visibleArtifactIds: z.array(ReleaseIdSchema).max(100_000),
|
|
115
120
|
}).strict().superRefine((candidate, context) => {
|
|
116
121
|
if (new Set(candidate.visibleArtifactIds).size !== candidate.visibleArtifactIds.length) {
|
|
117
122
|
context.addIssue({
|
|
@@ -158,14 +163,24 @@ export const ComparisonAssignmentSchema = ComparisonAssignmentBaseSchema
|
|
|
158
163
|
}
|
|
159
164
|
});
|
|
160
165
|
const PreferenceOrderSchema = z.array(z.array(ReleaseIdSchema).min(1).max(4)).max(4);
|
|
166
|
+
const PreferenceReviewerSchema = z.discriminatedUnion("kind", [
|
|
167
|
+
z.object({
|
|
168
|
+
kind: z.enum(["human", "model", "deterministic"]),
|
|
169
|
+
releaseRef: ImmutableReleaseRefSchema,
|
|
170
|
+
}).strict(),
|
|
171
|
+
z.object({
|
|
172
|
+
kind: z.literal("fixture"),
|
|
173
|
+
releaseRef: ImmutableReleaseRefSchema,
|
|
174
|
+
fixtureRelease: ImmutableReleaseRefSchema,
|
|
175
|
+
labelSource: z.literal("synthetic"),
|
|
176
|
+
qualificationEligibility: z.literal("smoke_only"),
|
|
177
|
+
}).strict(),
|
|
178
|
+
]);
|
|
161
179
|
const PreferenceReceiptBaseSchema = z.object({
|
|
162
180
|
schemaVersion: z.literal("openpond.preferenceReceipt.v1"),
|
|
163
181
|
id: ReleaseIdSchema,
|
|
164
182
|
assignmentRef: ImmutableReleaseRefSchema,
|
|
165
|
-
reviewer:
|
|
166
|
-
kind: PreferenceReviewerKindSchema,
|
|
167
|
-
releaseRef: ImmutableReleaseRefSchema,
|
|
168
|
-
}).strict(),
|
|
183
|
+
reviewer: PreferenceReviewerSchema,
|
|
169
184
|
order: PreferenceOrderSchema,
|
|
170
185
|
rejectAll: z.boolean(),
|
|
171
186
|
criterionScores: z.record(ReleaseIdSchema, z.record(ReleaseIdSchema, z.number().finite().min(0).max(1))),
|
|
@@ -244,7 +259,8 @@ export function createComparisonAssignment(input) {
|
|
|
244
259
|
if (input.candidates.length !== release.candidateCount) {
|
|
245
260
|
throw new Error("Comparison assignment candidate count does not match the immutable comparison release.");
|
|
246
261
|
}
|
|
247
|
-
const
|
|
262
|
+
const requiresVisibleArtifact = release.presentation.parts.some((part) => part.source === "artifact");
|
|
263
|
+
const normalized = input.candidates.map((candidate) => validateComparisonCandidate(candidate, taskset, requiresVisibleArtifact));
|
|
248
264
|
const first = normalized[0];
|
|
249
265
|
for (const candidate of normalized.slice(1)) {
|
|
250
266
|
if (candidate.attempt.taskId !== first.attempt.taskId) {
|
|
@@ -331,6 +347,45 @@ export function createPreferenceReceipt(input) {
|
|
|
331
347
|
validateCriterionScores(content.criterionScores, assignment, release);
|
|
332
348
|
return PreferenceReceiptSchema.parse({ ...content, contentHash: contentHash(content) });
|
|
333
349
|
}
|
|
350
|
+
export function createSyntheticFixturePreferenceReceipt(input) {
|
|
351
|
+
const candidateIds = input.assignment.candidates.map((candidate) => candidate.attemptRef.id);
|
|
352
|
+
const ratingIds = Object.keys(input.ratings);
|
|
353
|
+
if (ratingIds.length !== candidateIds.length
|
|
354
|
+
|| candidateIds.some((candidateId) => !input.ratings[candidateId])
|
|
355
|
+
|| ratingIds.some((candidateId) => !candidateIds.includes(candidateId))) {
|
|
356
|
+
throw new Error("Synthetic fixture ratings must label every assignment candidate exactly once.");
|
|
357
|
+
}
|
|
358
|
+
const orderedRatings = ["love", "like", "reject"];
|
|
359
|
+
const rejectAll = candidateIds.every((candidateId) => input.ratings[candidateId] === "reject");
|
|
360
|
+
const order = rejectAll ? [] : orderedRatings
|
|
361
|
+
.map((rating) => candidateIds.filter((candidateId) => input.ratings[candidateId] === rating))
|
|
362
|
+
.filter((bucket) => bucket.length > 0);
|
|
363
|
+
const score = { love: 1, like: 0.5, reject: 0 };
|
|
364
|
+
return createPreferenceReceipt({
|
|
365
|
+
id: input.id,
|
|
366
|
+
assignment: input.assignment,
|
|
367
|
+
comparisonRelease: input.comparisonRelease,
|
|
368
|
+
reviewer: {
|
|
369
|
+
kind: "fixture",
|
|
370
|
+
releaseRef: input.labelerRelease,
|
|
371
|
+
fixtureRelease: input.fixtureRelease,
|
|
372
|
+
labelSource: "synthetic",
|
|
373
|
+
qualificationEligibility: "smoke_only",
|
|
374
|
+
},
|
|
375
|
+
order,
|
|
376
|
+
rejectAll,
|
|
377
|
+
criterionScores: Object.fromEntries(candidateIds.map((candidateId) => [
|
|
378
|
+
candidateId,
|
|
379
|
+
Object.fromEntries(input.comparisonRelease.criteria.map((criterion) => [
|
|
380
|
+
criterion.id,
|
|
381
|
+
score[input.ratings[candidateId]],
|
|
382
|
+
])),
|
|
383
|
+
])),
|
|
384
|
+
startedAt: input.startedAt,
|
|
385
|
+
completedAt: input.completedAt,
|
|
386
|
+
metadata: input.metadata ?? {},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
334
389
|
export function verifyPreferenceReceipt(value) {
|
|
335
390
|
return verifyHashed(value, PreferenceReceiptContentSchema, PreferenceReceiptSchema);
|
|
336
391
|
}
|
|
@@ -436,12 +491,16 @@ export function createPreferenceRewardComponents(input) {
|
|
|
436
491
|
? input.result
|
|
437
492
|
: null;
|
|
438
493
|
const automatedReceipt = modelReceipt?.reviewer.kind === "model";
|
|
439
|
-
const
|
|
494
|
+
const fixtureReceipt = modelReceipt?.reviewer.kind === "fixture";
|
|
495
|
+
const rewardScope = PreferenceRewardScopeSchema.parse(input.rewardScope ?? "production");
|
|
496
|
+
const admittedReviewer = fixtureReceipt
|
|
497
|
+
? rewardScope === "synthetic_smoke"
|
|
498
|
+
: !automatedReceipt || isCalibratedAutomatedReviewer(modelReceipt, release, input.calibrationReport ?? null);
|
|
440
499
|
return Object.fromEntries(assignment.candidates.map((candidate) => {
|
|
441
500
|
const candidateEligibility = eligible.get(candidate.attemptRef.id);
|
|
442
501
|
const canScore = candidateEligibility?.eligible ?? true;
|
|
443
502
|
const score = scores[candidate.attemptRef.id];
|
|
444
|
-
const component = canScore &&
|
|
503
|
+
const component = canScore && admittedReviewer
|
|
445
504
|
? RewardComponentReceiptSchema.parse({
|
|
446
505
|
verifierId: release.rewardProjection.verifierId,
|
|
447
506
|
verifierVersion: release.rewardProjection.verifierVersion,
|
|
@@ -475,7 +534,11 @@ export function createPreferenceRewardComponents(input) {
|
|
|
475
534
|
rewardEligible: false,
|
|
476
535
|
rewardContribution: null,
|
|
477
536
|
failureOwner: candidateEligibility?.failureOwner ?? "verifier",
|
|
478
|
-
feedback: [canScore
|
|
537
|
+
feedback: [canScore
|
|
538
|
+
? fixtureReceipt
|
|
539
|
+
? "Synthetic fixture preference evidence is admitted only to synthetic-smoke reward scope."
|
|
540
|
+
: "Automated preference reviewer is uncalibrated or inconsistent."
|
|
541
|
+
: "Candidate is not eligible for comparative preference scoring."],
|
|
479
542
|
visibleEvidenceRefs: [],
|
|
480
543
|
privilegedEvidenceRefs: [],
|
|
481
544
|
metadata: {
|
|
@@ -487,7 +550,7 @@ export function createPreferenceRewardComponents(input) {
|
|
|
487
550
|
return [candidate.attemptRef.id, component];
|
|
488
551
|
}));
|
|
489
552
|
}
|
|
490
|
-
function validateComparisonCandidate(input, taskset) {
|
|
553
|
+
function validateComparisonCandidate(input, taskset, requiresVisibleArtifact) {
|
|
491
554
|
const attempt = AttemptReceiptSchema.parse(input.attempt);
|
|
492
555
|
if (!verifyAttemptReceipt(attempt))
|
|
493
556
|
throw new Error("Comparison candidate Attempt Receipt has an invalid content hash.");
|
|
@@ -514,7 +577,10 @@ function validateComparisonCandidate(input, taskset) {
|
|
|
514
577
|
const available = new Set(artifactManifest.entries
|
|
515
578
|
.filter((entry) => entry.status === "collected" && entry.artifact !== null)
|
|
516
579
|
.map((entry) => entry.artifact.id));
|
|
517
|
-
if (!visibleArtifactIds.length
|
|
580
|
+
if (requiresVisibleArtifact && !visibleArtifactIds.length) {
|
|
581
|
+
throw new Error("Comparison presentation requires each candidate to expose a reviewable artifact.");
|
|
582
|
+
}
|
|
583
|
+
if (visibleArtifactIds.some((id) => !available.has(id))) {
|
|
518
584
|
throw new Error("Comparison candidate exposes an artifact that is missing, uncollected, or unreviewable.");
|
|
519
585
|
}
|
|
520
586
|
if (visible.size !== visibleArtifactIds.length)
|