@openpond/evals 0.12.5 → 0.12.7

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 (47) hide show
  1. package/LEARNING.md +5 -0
  2. package/dist/javascript-isolate-process-source.js +1 -1
  3. package/dist/javascript-verifier-worker-source.js +1 -1
  4. package/dist/learning/admission.js +1 -1
  5. package/dist/learning/contracts.js +12 -0
  6. package/dist/learning/iteration-dispatch-contracts.js +4 -0
  7. package/dist/learning/iteration-dispatch-worker.js +4 -0
  8. package/dist/learning/iteration-eligibility.js +2 -1
  9. package/dist/learning/iteration-exposure.js +30 -0
  10. package/dist/learning/iteration-reservation-cancellation.js +12 -3
  11. package/dist/learning/iteration-reservation-contracts.js +5 -3
  12. package/dist/learning/iteration-reservation-service.js +10 -2
  13. package/dist/learning/iteration-training-parent.js +3 -1
  14. package/dist/learning/reward-authoring.js +4 -2
  15. package/dist/learning/service.js +2 -0
  16. package/dist/sql-execution-process-source.js +1 -1
  17. package/dist/task-schema-validation.js +10 -2
  18. package/dist/types/learning/batch-service.d.ts +1 -0
  19. package/dist/types/learning/batch-service.d.ts.map +1 -1
  20. package/dist/types/learning/contracts.d.ts +42 -0
  21. package/dist/types/learning/contracts.d.ts.map +1 -1
  22. package/dist/types/learning/iteration-dispatch-contracts.d.ts +13 -0
  23. package/dist/types/learning/iteration-dispatch-contracts.d.ts.map +1 -1
  24. package/dist/types/learning/iteration-dispatch-worker.d.ts.map +1 -1
  25. package/dist/types/learning/iteration-eligibility.d.ts.map +1 -1
  26. package/dist/types/learning/iteration-exposure.d.ts +13 -0
  27. package/dist/types/learning/iteration-exposure.d.ts.map +1 -0
  28. package/dist/types/learning/iteration-reservation-cancellation.d.ts.map +1 -1
  29. package/dist/types/learning/iteration-reservation-contracts.d.ts +13 -3
  30. package/dist/types/learning/iteration-reservation-contracts.d.ts.map +1 -1
  31. package/dist/types/learning/iteration-reservation-service.d.ts.map +1 -1
  32. package/dist/types/learning/iteration-training-parent.d.ts.map +1 -1
  33. package/dist/types/learning/operations.d.ts +38 -0
  34. package/dist/types/learning/operations.d.ts.map +1 -1
  35. package/dist/types/learning/repository.d.ts +28 -2
  36. package/dist/types/learning/repository.d.ts.map +1 -1
  37. package/dist/types/learning/reward-authoring.d.ts.map +1 -1
  38. package/dist/types/learning/service.d.ts.map +1 -1
  39. package/dist/types/learning/transport.d.ts +1009 -970
  40. package/dist/types/learning/transport.d.ts.map +1 -1
  41. package/dist/types/task-schema-validation.d.ts.map +1 -1
  42. package/package.json +2 -2
  43. package/schemas/learning/v1/command-request.schema.json +119 -93
  44. package/schemas/learning/v1/consumption.schema.json +40 -4
  45. package/schemas/learning/v1/evidence.schema.json +3 -0
  46. package/schemas/learning/v1/example-submission.schema.json +3 -0
  47. package/schemas/learning/v1/source.schema.json +20 -0
@@ -88,7 +88,7 @@ export function taskRecordFromEvidence(evidence, definition) {
88
88
  return TaskRecordSchema.parse({
89
89
  id: evidence.id, clusterKey: evidence.submission.familyKey, split: evidence.submission.split,
90
90
  input: evidence.submission.input, expectedOutput: evidence.submission.expected,
91
- policyVisibleContext: { instructions: definition.instructions },
91
+ policyVisibleContext: evidence.submission.policyVisibleContext ?? { instructions: definition.instructions },
92
92
  privilegedContextRef: evidence.submission.evaluatorContext === null ? null : `task-evidence:${evidence.contentHash}`,
93
93
  artifactRefs: evidence.submission.assets, tags: [definition.category],
94
94
  ...(requiredOutputs === undefined ? {} : { requiredOutputs }),
@@ -89,6 +89,13 @@ export const LearningReviewOriginSchema = z
89
89
  rewardBinding: LearningRevisionRefSchema,
90
90
  })
91
91
  .strict();
92
+ /** A host must verify this retained import receipt before resolving package
93
+ * bytes. This portable reference is neither an approval nor an access grant. */
94
+ export const LearningDatasetOriginSchema = z.object({
95
+ receiptId: ReleaseIdSchema,
96
+ packageHash: ReleaseHashSchema,
97
+ taskset: LearningRevisionRefSchema,
98
+ }).strict();
92
99
  export const LearningSourceContentSchema = z
93
100
  .object({
94
101
  schemaVersion: z.literal("openpond.learningSource.v1"),
@@ -102,6 +109,7 @@ export const LearningSourceContentSchema = z
102
109
  mapping: TaskSourceMappingSchema.nullable(),
103
110
  adapterVersion: z.string().trim().min(1).max(200).nullable(),
104
111
  reviewOrigin: LearningReviewOriginSchema.optional(),
112
+ datasetOrigin: LearningDatasetOriginSchema.optional(),
105
113
  /** Host-retained authorization for materializing neutral conversation
106
114
  * evidence. The receipt is reauthorized at preparation and paid admission;
107
115
  * this portable reference alone never grants access or training consent. */
@@ -115,6 +123,8 @@ export const LearningSourceContentSchema = z
115
123
  })
116
124
  .strict()
117
125
  .superRefine((source, context) => {
126
+ if (source.datasetOrigin && (source.reviewOrigin || source.capturedOrigin))
127
+ context.addIssue({ code: "custom", path: ["datasetOrigin"], message: "Choose one retained source origin." });
118
128
  if (source.kind !== "direct" && !source.mapping)
119
129
  context.addIssue({
120
130
  code: "custom",
@@ -149,6 +159,8 @@ export const TaskExampleSubmissionSchema = z
149
159
  familyKey: ReleaseIdSchema.nullable(),
150
160
  split: TaskSplitSchema,
151
161
  input: LearningJsonObjectSchema,
162
+ /** Per-example policy context remains separate from evaluator-only data. */
163
+ policyVisibleContext: LearningJsonObjectSchema.optional(),
152
164
  observedOutput: LearningJsonObjectSchema.nullable(),
153
165
  expected: LearningJsonObjectSchema.nullable(),
154
166
  evaluatorContext: LearningJsonObjectSchema.nullable(),
@@ -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);
@@ -1,4 +1,4 @@
1
- import { RewardReleaseSchema, createRewardRelease, feedbackKeyForReward } from "../rewards.js";
1
+ import { RewardReleaseSchema, createRewardRelease } from "../rewards.js";
2
2
  import { assertBoundedTaskJson } from "../task-schema.js";
3
3
  import { RewardAuthoringFieldsSchema, RewardFixtureAuthoringFieldsSchema } from "./authoring.js";
4
4
  import { createLearningTextAsset, verifyLearningTextAsset } from "./assets.js";
@@ -65,7 +65,9 @@ export function compileRewardAuthoring(input) {
65
65
  const assets = [...(asset ? [asset] : []), ...(fixtureAsset ? [fixtureAsset] : [])];
66
66
  const reward = createRewardRelease({
67
67
  schemaVersion: "openpond.rewardRelease.v1", id: input.id, revision: (base?.revision ?? 0) + 1,
68
- name: fields.name, description: fields.description, feedbackKey: fields.feedbackKey ?? feedbackKeyForReward({ id: input.id }), implementation,
68
+ // Derived defaults belong to feedbackKeyForReward at read time. Adding an
69
+ // unauthored field here changes the immutable calibration snapshot.
70
+ name: fields.name, description: fields.description, ...(fields.feedbackKey === undefined ? {} : { feedbackKey: fields.feedbackKey }), implementation,
69
71
  rawScore: fields.kind === "learned_model" ? { minimum: authoringNumber(fields.minimum, "Minimum score"), maximum: authoringNumber(fields.maximum, "Maximum score") } : { minimum: 0, maximum: 1 },
70
72
  assets: assets.map(value => value.asset), ...(fixtureAsset ? { fixtureSetRef: fixtureAsset.asset } : {}),
71
73
  ...(calibrationCheckRef ? { calibrationCheckRef } : {}),
@@ -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);