@openpond/evals 0.6.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 (87) hide show
  1. package/LEARNING.md +106 -0
  2. package/README.md +2 -0
  3. package/dist/graders.js +26 -5
  4. package/dist/index.js +3 -0
  5. package/dist/javascript-verifier-contract.js +7 -0
  6. package/dist/javascript-verifier-node.js +56 -0
  7. package/dist/javascript-verifier-worker-source.js +1 -0
  8. package/dist/javascript-verifier-worker.js +6 -0
  9. package/dist/javascript-verifier.js +64 -0
  10. package/dist/learning/admission.js +209 -0
  11. package/dist/learning/assets.js +37 -0
  12. package/dist/learning/contracts.js +267 -0
  13. package/dist/learning/errors.js +12 -0
  14. package/dist/learning/grade-worker.js +99 -0
  15. package/dist/learning/index.js +9 -0
  16. package/dist/learning/operations.js +33 -0
  17. package/dist/learning/repository.js +48 -0
  18. package/dist/learning/service.js +367 -0
  19. package/dist/learning/transport.js +25 -0
  20. package/dist/rewards.js +230 -0
  21. package/dist/task-schema.js +142 -0
  22. package/dist/tasksets.js +3 -0
  23. package/dist/telemetry-catalog.js +1 -1
  24. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +7 -0
  25. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  26. package/dist/types/conformance.d.ts +14 -0
  27. package/dist/types/conformance.d.ts.map +1 -1
  28. package/dist/types/execution-contracts.d.ts +14 -0
  29. package/dist/types/execution-contracts.d.ts.map +1 -1
  30. package/dist/types/graders.d.ts.map +1 -1
  31. package/dist/types/index.d.ts +3 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/javascript-verifier-contract.d.ts +9 -0
  34. package/dist/types/javascript-verifier-contract.d.ts.map +1 -0
  35. package/dist/types/javascript-verifier-node.d.ts +5 -0
  36. package/dist/types/javascript-verifier-node.d.ts.map +1 -0
  37. package/dist/types/javascript-verifier-worker-source.d.ts +2 -0
  38. package/dist/types/javascript-verifier-worker.d.ts +2 -0
  39. package/dist/types/javascript-verifier-worker.d.ts.map +1 -0
  40. package/dist/types/javascript-verifier.d.ts +15 -0
  41. package/dist/types/javascript-verifier.d.ts.map +1 -0
  42. package/dist/types/learning/admission.d.ts +335 -0
  43. package/dist/types/learning/admission.d.ts.map +1 -0
  44. package/dist/types/learning/assets.d.ts +49 -0
  45. package/dist/types/learning/assets.d.ts.map +1 -0
  46. package/dist/types/learning/contracts.d.ts +2149 -0
  47. package/dist/types/learning/contracts.d.ts.map +1 -0
  48. package/dist/types/learning/errors.d.ts +8 -0
  49. package/dist/types/learning/errors.d.ts.map +1 -0
  50. package/dist/types/learning/grade-worker.d.ts +31 -0
  51. package/dist/types/learning/grade-worker.d.ts.map +1 -0
  52. package/dist/types/learning/index.d.ts +10 -0
  53. package/dist/types/learning/index.d.ts.map +1 -0
  54. package/dist/types/learning/operations.d.ts +2061 -0
  55. package/dist/types/learning/operations.d.ts.map +1 -0
  56. package/dist/types/learning/repository.d.ts +1143 -0
  57. package/dist/types/learning/repository.d.ts.map +1 -0
  58. package/dist/types/learning/service.d.ts +22 -0
  59. package/dist/types/learning/service.d.ts.map +1 -0
  60. package/dist/types/learning/transport.d.ts +2139 -0
  61. package/dist/types/learning/transport.d.ts.map +1 -0
  62. package/dist/types/preferences.d.ts +3 -3
  63. package/dist/types/review-conformance.d.ts +5 -5
  64. package/dist/types/rewards.d.ts +611 -0
  65. package/dist/types/rewards.d.ts.map +1 -0
  66. package/dist/types/task-schema.d.ts +18 -0
  67. package/dist/types/task-schema.d.ts.map +1 -0
  68. package/dist/types/tasksets.d.ts +28 -0
  69. package/dist/types/tasksets.d.ts.map +1 -1
  70. package/package.json +30 -3
  71. package/schemas/learning/v1/asset.schema.json +89 -0
  72. package/schemas/learning/v1/batch.schema.json +132 -0
  73. package/schemas/learning/v1/binding.schema.json +207 -0
  74. package/schemas/learning/v1/command-request.schema.json +2414 -0
  75. package/schemas/learning/v1/decision.schema.json +440 -0
  76. package/schemas/learning/v1/definition.schema.json +397 -0
  77. package/schemas/learning/v1/evidence.schema.json +317 -0
  78. package/schemas/learning/v1/example-submission.schema.json +253 -0
  79. package/schemas/learning/v1/feedback-submission.schema.json +114 -0
  80. package/schemas/learning/v1/feedback.schema.json +233 -0
  81. package/schemas/learning/v1/grade.schema.json +412 -0
  82. package/schemas/learning/v1/iteration.schema.json +236 -0
  83. package/schemas/learning/v1/package.schema.json +776 -0
  84. package/schemas/learning/v1/policy.schema.json +373 -0
  85. package/schemas/learning/v1/read-request.schema.json +101 -0
  86. package/schemas/learning/v1/reward.schema.json +302 -0
  87. package/schemas/learning/v1/source.schema.json +198 -0
@@ -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
+ }
@@ -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();