@openpond/evals 0.12.6 → 0.12.8

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 (36) hide show
  1. package/LEARNING.md +5 -0
  2. package/RELEASING.md +6 -0
  3. package/dist/javascript-isolate-process-source.js +1 -1
  4. package/dist/javascript-verifier-worker-source.js +1 -1
  5. package/dist/learning/iteration-dispatch-contracts.js +4 -0
  6. package/dist/learning/iteration-dispatch-worker.js +4 -0
  7. package/dist/learning/iteration-eligibility.js +2 -1
  8. package/dist/learning/iteration-exposure.js +30 -0
  9. package/dist/learning/iteration-reservation-cancellation.js +12 -3
  10. package/dist/learning/iteration-reservation-contracts.js +5 -3
  11. package/dist/learning/iteration-reservation-service.js +10 -2
  12. package/dist/learning/iteration-training-parent.js +3 -1
  13. package/dist/learning/reward-authoring.js +10 -4
  14. package/dist/learning/service.js +2 -0
  15. package/dist/sql-execution-process-source.js +1 -1
  16. package/dist/task-schema-validation.js +10 -2
  17. package/dist/types/learning/iteration-dispatch-contracts.d.ts +13 -0
  18. package/dist/types/learning/iteration-dispatch-contracts.d.ts.map +1 -1
  19. package/dist/types/learning/iteration-dispatch-worker.d.ts.map +1 -1
  20. package/dist/types/learning/iteration-eligibility.d.ts.map +1 -1
  21. package/dist/types/learning/iteration-exposure.d.ts +13 -0
  22. package/dist/types/learning/iteration-exposure.d.ts.map +1 -0
  23. package/dist/types/learning/iteration-reservation-cancellation.d.ts.map +1 -1
  24. package/dist/types/learning/iteration-reservation-contracts.d.ts +13 -3
  25. package/dist/types/learning/iteration-reservation-contracts.d.ts.map +1 -1
  26. package/dist/types/learning/iteration-reservation-service.d.ts.map +1 -1
  27. package/dist/types/learning/iteration-training-parent.d.ts.map +1 -1
  28. package/dist/types/learning/repository.d.ts +18 -2
  29. package/dist/types/learning/repository.d.ts.map +1 -1
  30. package/dist/types/learning/reward-authoring.d.ts.map +1 -1
  31. package/dist/types/learning/service.d.ts.map +1 -1
  32. package/dist/types/learning/transport.d.ts +1004 -994
  33. package/dist/types/learning/transport.d.ts.map +1 -1
  34. package/dist/types/task-schema-validation.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/schemas/learning/v1/consumption.schema.json +40 -4
@@ -89,7 +89,11 @@ export const LearningIterationObservationSchema = z.object({
89
89
  evaluation: ImmutableReleaseRefSchema.nullable(),
90
90
  candidate: ImmutableReleaseRefSchema.nullable(),
91
91
  failure: z.string().max(20_000).nullable(),
92
+ coverage: z.array(z.object({ evidence: LearningRevisionRefSchema,
93
+ outcome: z.enum(["trained", "exposed", "unexposed", "unknown"]) }).strict()).max(10_000).optional(),
92
94
  }).strict().superRefine((value, context) => {
95
+ if (value.coverage && (!["succeeded", "failed", "cancelled"].includes(value.status) || !value.cleanupComplete || !value.receipt))
96
+ context.addIssue({ code: "custom", path: ["coverage"], message: "Coverage requires authoritative terminal cleanup evidence." });
93
97
  if (["succeeded", "failed", "cancelled"].includes(value.status) && (!value.cleanupComplete || !value.receipt)) {
94
98
  context.addIssue({ code: "custom", path: ["receipt"], message: "Terminal execution requires an authoritative cleanup receipt." });
95
99
  }
@@ -6,6 +6,7 @@ import { LearningIterationAdmissionSchema, LearningIterationDispatchSchema, Lear
6
6
  import { requireLearningRelease, requireLearningResource } from "./repository.js";
7
7
  import { deferLearningIterationSubmission } from "./iteration-dispatch-deferral.js";
8
8
  import { LearningIterationSubmissionDeferralSchema } from "./iteration-dispatch-contracts.js";
9
+ import { settleLearningIterationExposure } from "./iteration-exposure.js";
9
10
  const closed = new Set(["accepted", "rejected", "failed", "cancelled", "completed_without_candidate"]);
10
11
  export function createLearningIterationWorker(repository, executor, options) {
11
12
  const now = options.now ?? (() => new Date().toISOString());
@@ -85,6 +86,9 @@ export function createLearningIterationWorker(repository, executor, options) {
85
86
  terminalReceipt: outcome?.receipt ?? null, leaseOwner: null, leaseExpiresAt: null, admission: null,
86
87
  nextAttemptAt: terminal ? null : new Date(Date.parse(now()) + 5_000).toISOString(), consecutiveFailures: 0, lastError: null });
87
88
  if (terminal) {
89
+ await settleLearningIterationExposure(tx, { chainId: current.chainId, iterationId: iteration.id,
90
+ batch: await requireLearningRelease(tx, "batch", current.batch), outcome,
91
+ unstarted: !outcome && !current.submissionStartedAt });
88
92
  const reservation = await requireLearningResource(tx, "reservation", iteration.id);
89
93
  const settled = LearningIterationReservationSchema.parse({ ...reservation, revision: reservation.revision + 1,
90
94
  budget: { ...reservation.budget, reservedSpendUsd: 0, settledSpendUsd: updated.spendUsd, settledAt: now() }, updatedAt: now() });
@@ -46,7 +46,8 @@ export async function inspectIterationEligibility(transaction, policy, options =
46
46
  counts.excluded += 1;
47
47
  continue;
48
48
  }
49
- if (await transaction.get("consumption", learningConsumptionId(chainId, evidence))) {
49
+ const consumption = await transaction.get("consumption", learningConsumptionId(chainId, evidence));
50
+ if (consumption && consumption.exposure !== "unexposed") {
50
51
  counts.consumed += 1;
51
52
  continue;
52
53
  }
@@ -0,0 +1,30 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { sameLearningRef } from "./contracts.js";
3
+ import { LearningDomainError } from "./errors.js";
4
+ import { LearningConsumptionSchema } from "./iteration-reservation-contracts.js";
5
+ /** Called only in fenced terminal settlement. Absence/timeout is never evidence
6
+ * of zero exposure; unresolved rows keep their original reservation. */
7
+ export async function settleLearningIterationExposure(tx, input) {
8
+ const coverage = input.outcome?.coverage;
9
+ if (!input.unstarted && !coverage)
10
+ return;
11
+ if (coverage && (coverage.length !== input.batch.examples.length ||
12
+ new Set(coverage.map(entry => contentHash(entry.evidence))).size !== coverage.length ||
13
+ coverage.some(entry => !input.batch.examples.some(example => sameLearningRef(example.evidence, entry.evidence)))))
14
+ throw new LearningDomainError("learning_execution_coverage_mismatch", 409);
15
+ for (const example of input.batch.examples) {
16
+ const id = `consumption-${contentHash([input.chainId, example.evidence.id, example.evidence.revision])}`;
17
+ const current = await tx.get("consumption", id);
18
+ if (!current || current.iterationId !== input.iterationId || !sameLearningRef(current.evidence, example.evidence))
19
+ throw new LearningDomainError("learning_execution_consumption_mismatch", 409);
20
+ const exposure = input.unstarted ? "unexposed" : coverage.find(entry => sameLearningRef(entry.evidence, example.evidence)).outcome;
21
+ if (exposure === "unknown")
22
+ continue;
23
+ // A terminal replay cannot downgrade already proven numerical exposure.
24
+ if (["trained", "exposed"].includes(current.exposure ?? "") && exposure !== current.exposure)
25
+ throw new LearningDomainError("learning_execution_coverage_conflict", 409);
26
+ const next = LearningConsumptionSchema.parse({ ...current, revision: current.revision + 1,
27
+ exposure, executionReceipt: input.outcome?.receipt ?? null });
28
+ await tx.put("consumption", next, current.revision, { parentId: input.chainId });
29
+ }
30
+ }
@@ -1,7 +1,8 @@
1
1
  import { LearningIterationSchema } from "./contracts.js";
2
2
  import { LearningDomainError } from "./errors.js";
3
3
  import { LearningChainSchema, LearningIterationReservationSchema } from "./iteration-reservation-contracts.js";
4
- import { LearningConflictError, requireLearningResource } from "./repository.js";
4
+ import { LearningConflictError, requireLearningRelease, requireLearningResource } from "./repository.js";
5
+ import { settleLearningIterationExposure } from "./iteration-exposure.js";
5
6
  /** Only a reservation that has never been dispatched can release spend locally.
6
7
  * Submitted executions require authoritative cancellation and settlement. */
7
8
  export async function cancelLearningIterationReservation(transaction, input, now) {
@@ -14,6 +15,9 @@ export async function cancelLearningIterationReservation(transaction, input, now
14
15
  if (iteration.trainingJob || iteration.evaluationJob || !["ready", "waiting_for_data", "waiting_for_review"].includes(iteration.status)) {
15
16
  throw new LearningDomainError("learning_execution_cancellation_required", 409);
16
17
  }
18
+ const dispatch = await transaction.get("dispatch", iteration.dispatchId);
19
+ if (dispatch?.submissionStartedAt || dispatch?.execution || dispatch?.leaseOwner)
20
+ throw new LearningDomainError("learning_execution_cancellation_required", 409);
17
21
  const updated = LearningIterationSchema.parse({ ...iteration, revision: iteration.revision + 1, status: "cancelled", updatedAt: now });
18
22
  const released = LearningIterationReservationSchema.parse({
19
23
  ...reservation, revision: reservation.revision + 1,
@@ -25,7 +29,12 @@ export async function cancelLearningIterationReservation(transaction, input, now
25
29
  if (chain.activeIterationId === iteration.id) {
26
30
  await transaction.put("chain", LearningChainSchema.parse({ ...chain, revision: chain.revision + 1, activeIterationId: null, updatedAt: now }), chain.revision, { parentId: chain.modelProjectId });
27
31
  }
28
- // Consumption is retained. Retry/replay is an explicit operation, never a
29
- // side effect of cancelling that silently makes the same examples new again.
32
+ // Retain the reservation history; only this transaction's fenced proof that
33
+ // dispatch never started makes its exact evidence eligible again.
34
+ if (iteration.batch)
35
+ await settleLearningIterationExposure(transaction, {
36
+ chainId: reservation.chainId, iterationId: iteration.id,
37
+ batch: await requireLearningRelease(transaction, "batch", iteration.batch), outcome: null, unstarted: true,
38
+ });
30
39
  return [{ kind: "iteration", id: updated.id, revision: updated.revision }, { kind: "reservation", id: released.id, revision: released.revision }];
31
40
  }
@@ -24,17 +24,19 @@ export const LearningEligibilityCountsSchema = z.object({
24
24
  excluded: z.number().int().nonnegative(),
25
25
  consumed: z.number().int().nonnegative(),
26
26
  }).strict();
27
- /** Immutable evidence consumption survives rejection and policy revision changes.
28
- * A corrected evidence revision is new data; reapproving the same revision isn't. */
27
+ /** Reservation history is retained; authoritative terminal exposure alone may
28
+ * release proven-unexposed evidence. Ambiguous execution remains reserved. */
29
29
  export const LearningConsumptionSchema = z.object({
30
30
  schemaVersion: z.literal("openpond.learningConsumption.v1"),
31
31
  id: ReleaseIdSchema,
32
- revision: z.literal(1),
32
+ revision: z.number().int().positive(),
33
33
  chainId: ReleaseIdSchema,
34
34
  iterationId: ReleaseIdSchema,
35
35
  evidence: LearningRevisionRefSchema,
36
36
  decision: LearningRevisionRefSchema,
37
37
  reservedAt: ReleaseTimestampSchema,
38
+ exposure: z.enum(["reserved", "trained", "exposed", "unexposed"]).optional(),
39
+ executionReceipt: z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict().nullable().optional(),
38
40
  }).strict();
39
41
  /** Dispatch must reconcile this reservation, never reserve a second batch on retry. */
40
42
  export const LearningIterationReservationSchema = z.object({
@@ -24,6 +24,8 @@ export async function reserveLearningIteration(transaction, input, actorId, now)
24
24
  return [{ kind: "iteration", id, revision: (await requireLearningResource(transaction, "iteration", id)).revision }, { kind: "reservation", id, revision: previous.revision }];
25
25
  }
26
26
  const current = await requireLearningResource(transaction, "policy", policy.id);
27
+ if (policy.training.replayBatches.length)
28
+ throw new LearningDomainError("learning_replay_not_supported", 422);
27
29
  if (!sameLearningRef(learningRef(current), input.policy))
28
30
  throw new LearningDomainError("learning_policy_revision_stale", 409);
29
31
  if (!policy.enabled)
@@ -58,6 +60,7 @@ export async function reserveLearningIteration(transaction, input, actorId, now)
58
60
  const parent = await resolveLearningTrainingParent(transaction, policy, acceptedParent);
59
61
  let batch = null;
60
62
  if (ready) {
63
+ await transaction.validateTrainingReservation?.({ policy, parent: parent.trainingParentSelection });
61
64
  await assertAvailableBudget(transaction, chainId, now, policy.limits.maxIterationSpendUsd, policy.limits.maxDailySpendUsd);
62
65
  const sealed = await sealLearningBatch(transaction, {
63
66
  action: "seal_batch", operationId: input.operationId, batchId: `batch-${contentHash(id)}`,
@@ -68,11 +71,16 @@ export async function reserveLearningIteration(transaction, input, actorId, now)
68
71
  pointers.push(...sealed);
69
72
  batch = learningRef(await requireLearningResource(transaction, "batch", sealed[0].id));
70
73
  for (const item of eligibility.selected) {
74
+ const consumptionId = learningConsumptionId(chainId, item.evidence);
75
+ const prior = await transaction.get("consumption", consumptionId);
76
+ if (prior && prior.exposure !== "unexposed")
77
+ throw new LearningDomainError("learning_evidence_already_reserved", 409);
71
78
  const consumption = LearningConsumptionSchema.parse({
72
- schemaVersion: "openpond.learningConsumption.v1", id: learningConsumptionId(chainId, item.evidence), revision: 1,
79
+ schemaVersion: "openpond.learningConsumption.v1", id: consumptionId, revision: (prior?.revision ?? 0) + 1,
73
80
  chainId, iterationId: id, evidence: learningRef(item.evidence), decision: learningRef(item.decision), reservedAt: now,
81
+ exposure: "reserved", executionReceipt: null,
74
82
  });
75
- await transaction.put("consumption", consumption, 0, { parentId: chainId });
83
+ await transaction.put("consumption", consumption, prior?.revision ?? 0, { parentId: chainId });
76
84
  }
77
85
  }
78
86
  const iteration = LearningIterationSchema.parse({
@@ -29,7 +29,9 @@ export async function latestAcceptedLearningParent(tx, chainId, requireComplete
29
29
  /** Reservation captures an accepted revision before dispatch or later reviews.
30
30
  * Switching the underlying base requires a separately reviewed learning chain. */
31
31
  export async function resolveLearningTrainingParent(tx, policy, selection) {
32
- if (!selection)
32
+ // Supervised iterations retrain the reviewed batch from the pinned base.
33
+ // Accepted SFT candidates remain review evidence, never a GRPO checkpoint handoff.
34
+ if (!selection || policy.training.method === "sft")
33
35
  return { trainingParent: policy.trainingParent, trainingParentSelection: null };
34
36
  const parent = await requireLearningResource(tx, "iteration", selection.iterationId, selection.iterationRevision);
35
37
  const sourcePolicy = await requireLearningRelease(tx, "policy", parent.policy);
@@ -5,6 +5,7 @@ import { createLearningTextAsset, verifyLearningTextAsset } from "./assets.js";
5
5
  import { LearningJsonObjectSchema } from "./contracts.js";
6
6
  import { LearningDomainError } from "./errors.js";
7
7
  import { HumanFormSchema } from "../human-review/contracts.js";
8
+ import { sha256 } from "@openpond/harness";
8
9
  export const DEFAULT_REWARD_VERIFIER_SOURCE = `export function verify({ output, expectedOutput }) {
9
10
  if (!expectedOutput || !Object.hasOwn(expectedOutput, "answer")) {
10
11
  throw new Error("An expected answer is required.");
@@ -19,19 +20,19 @@ export function compileRewardAuthoring(input) {
19
20
  const base = input.base ? RewardReleaseSchema.parse(input.base) : null;
20
21
  if (base && base.id !== input.id)
21
22
  throw new LearningDomainError("reward_authoring_base_mismatch", 422);
22
- const fixtureAsset = fields.fixtures?.length ? createLearningTextAsset({ text: JSON.stringify(fields.fixtures), path: "reward-fixtures.json", mediaType: "application/json", visibility: "verifier" }) : null;
23
+ const fixtureAsset = fields.fixtures?.length ? rewardAsset(JSON.stringify(fields.fixtures), "reward-fixtures.json", "application/json") : null;
23
24
  let asset = null;
24
25
  let implementation;
25
26
  let calibrationCheckRef;
26
27
  if (fields.kind === "custom_verifier") {
27
- asset = createLearningTextAsset({ text: fields.code, path: "verifier.mjs", mediaType: "application/javascript", visibility: "verifier" });
28
+ asset = rewardAsset(fields.code, "verifier.mjs", "application/javascript");
28
29
  implementation = { kind: fields.kind, verifierRef: asset.asset, exportName: fields.exportName, timeoutMs: authoringNumber(fields.timeout, "Time limit"), networkPolicy: "none",
29
30
  ...((fields.verifierRuntime ?? (base?.implementation.kind === "custom_verifier" ? base.implementation.runtime : undefined)) ? { runtime: fields.verifierRuntime ?? (base?.implementation.kind === "custom_verifier" ? base.implementation.runtime : undefined) } : {}) };
30
31
  }
31
32
  else if (fields.kind === "model_judge" || fields.kind === "human") {
32
33
  if (!fields.rubric.trim())
33
34
  throw new LearningDomainError("reward_rubric_required", 422, "Write the rubric before checking or publishing this Reward.");
34
- asset = createLearningTextAsset({ text: fields.rubric, path: "rubric.md", mediaType: "text/markdown", visibility: "verifier" });
35
+ asset = rewardAsset(fields.rubric, "rubric.md", "text/markdown");
35
36
  if (fields.kind === "human")
36
37
  implementation = { kind: fields.kind, rubricRef: asset.asset, reviewerRole: fields.reviewerRole, form: HumanFormSchema.parse(JSON.parse(fields.humanForm || JSON.stringify({ schemaVersion: "openpond.humanForm.v1", mode: "individual", instructions: fields.rubric, criteria: [{ id: "quality", label: "Quality", instructions: fields.rubric, required: true, allowAbstain: true, kind: "score", minimum: 0, maximum: 1, step: 0.1 }] }))) };
37
38
  else {
@@ -49,7 +50,7 @@ export function compileRewardAuthoring(input) {
49
50
  }
50
51
  else if (fields.kind === "learned_model") {
51
52
  parseRewardAuthoringObject(fields.inputContract, "Model input contract");
52
- asset = createLearningTextAsset({ text: fields.inputContract, path: "input-contract.json", mediaType: "application/json", visibility: "verifier" });
53
+ asset = rewardAsset(fields.inputContract, "input-contract.json", "application/json");
53
54
  implementation = { kind: fields.kind, modelVersion: { id: fields.learnedId, contentHash: fields.learnedHash }, inputContract: asset.asset };
54
55
  }
55
56
  else if (fields.kind === "state")
@@ -74,6 +75,11 @@ export function compileRewardAuthoring(input) {
74
75
  });
75
76
  return { reward, assets };
76
77
  }
78
+ // Training and evaluation Rewards (including different revisions of one Reward)
79
+ // share a private asset directory. Different bytes must never claim one path.
80
+ function rewardAsset(text, filename, mediaType) {
81
+ return createLearningTextAsset({ text, path: `reward-assets/${sha256(text)}/${filename}`, mediaType, visibility: "verifier" });
82
+ }
77
83
  export function rewardAuthoringFields(reward, sourceAsset, fixtureAsset = null) {
78
84
  const implementation = reward?.implementation;
79
85
  const ref = implementation && "verifierRef" in implementation ? implementation.verifierRef
@@ -178,6 +178,8 @@ export function createLearningService(repository, options = {}) {
178
178
  break;
179
179
  }
180
180
  case "policy": {
181
+ if (input.content.training.replayBatches.length)
182
+ throw new LearningDomainError("learning_replay_not_supported", 422, "Replay requires an executor that materializes exact examples and weights.");
181
183
  const definition = await requireLearningRelease(transaction, "definition", input.content.taskDefinition);
182
184
  if (!sameLearningRef(definition.rewardBinding, input.content.rewardBinding))
183
185
  throw new LearningDomainError("learning_policy_binding_mismatch", 422);