@openpond/evals 0.10.9 → 0.11.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/LEARNING.md +35 -0
- package/dist/graders.js +19 -4
- package/dist/learning/budgeted-reward-fixtures.js +34 -0
- package/dist/learning/contracts.js +5 -0
- package/dist/learning/grade-judge-budget.js +26 -0
- package/dist/learning/index.js +8 -0
- package/dist/learning/intake-history.js +2 -1
- package/dist/learning/intake-text.js +14 -0
- package/dist/learning/intake.js +5 -2
- package/dist/learning/iteration-eligibility.js +5 -2
- package/dist/learning/iteration-reservation-contracts.js +1 -0
- package/dist/learning/iteration-reservation-service.js +11 -2
- package/dist/learning/judge-budget.js +59 -0
- package/dist/learning/model-judge.js +49 -0
- package/dist/learning/nightly-schedule.js +69 -0
- package/dist/learning/operations.js +3 -1
- package/dist/learning/reward-authoring.js +4 -0
- package/dist/learning/reward-calibration.js +66 -0
- package/dist/learning/reward-check-budget.js +27 -0
- package/dist/learning/reward-check-service.js +2 -2
- package/dist/learning/reward-check-worker.js +10 -2
- package/dist/learning/reward-checks.js +4 -1
- package/dist/learning/reward-fixture-executor.js +3 -2
- package/dist/learning/schedule-contracts.js +5 -1
- package/dist/learning/schedule-service.js +9 -5
- package/dist/learning/schedule-worker.js +22 -7
- package/dist/learning/service.js +40 -6
- package/dist/learning/task-queue-inspection.js +48 -0
- package/dist/learning/transport.js +1 -0
- package/dist/rewards.js +15 -3
- package/dist/rollouts.js +2 -2
- package/dist/types/graders.d.ts +51 -0
- package/dist/types/graders.d.ts.map +1 -1
- package/dist/types/learning/admission.d.ts +5 -0
- package/dist/types/learning/admission.d.ts.map +1 -1
- package/dist/types/learning/budgeted-reward-fixtures.d.ts +27 -0
- package/dist/types/learning/budgeted-reward-fixtures.d.ts.map +1 -0
- package/dist/types/learning/contracts.d.ts +313 -0
- package/dist/types/learning/contracts.d.ts.map +1 -1
- package/dist/types/learning/grade-judge-budget.d.ts +10 -0
- package/dist/types/learning/grade-judge-budget.d.ts.map +1 -0
- package/dist/types/learning/index.d.ts +8 -0
- package/dist/types/learning/index.d.ts.map +1 -1
- package/dist/types/learning/intake-history.d.ts.map +1 -1
- package/dist/types/learning/intake-text.d.ts +6 -0
- package/dist/types/learning/intake-text.d.ts.map +1 -0
- package/dist/types/learning/intake.d.ts +1 -0
- package/dist/types/learning/intake.d.ts.map +1 -1
- package/dist/types/learning/iteration-dispatch-contracts.d.ts +1 -1
- package/dist/types/learning/iteration-eligibility.d.ts +8 -1
- package/dist/types/learning/iteration-eligibility.d.ts.map +1 -1
- package/dist/types/learning/iteration-reservation-contracts.d.ts +7 -1
- package/dist/types/learning/iteration-reservation-contracts.d.ts.map +1 -1
- package/dist/types/learning/iteration-reservation-service.d.ts.map +1 -1
- package/dist/types/learning/judge-budget.d.ts +43 -0
- package/dist/types/learning/judge-budget.d.ts.map +1 -0
- package/dist/types/learning/model-judge.d.ts +29 -0
- package/dist/types/learning/model-judge.d.ts.map +1 -0
- package/dist/types/learning/nightly-schedule.d.ts +13 -0
- package/dist/types/learning/nightly-schedule.d.ts.map +1 -0
- package/dist/types/learning/operations.d.ts +63 -0
- package/dist/types/learning/operations.d.ts.map +1 -1
- package/dist/types/learning/repository.d.ts +188 -4
- package/dist/types/learning/repository.d.ts.map +1 -1
- package/dist/types/learning/reward-authoring.d.ts.map +1 -1
- package/dist/types/learning/reward-calibration.d.ts +129 -0
- package/dist/types/learning/reward-calibration.d.ts.map +1 -0
- package/dist/types/learning/reward-check-budget.d.ts +12 -0
- package/dist/types/learning/reward-check-budget.d.ts.map +1 -0
- package/dist/types/learning/reward-check-service.d.ts +1 -1
- package/dist/types/learning/reward-check-service.d.ts.map +1 -1
- package/dist/types/learning/reward-check-worker.d.ts.map +1 -1
- package/dist/types/learning/reward-checks.d.ts +84 -0
- package/dist/types/learning/reward-checks.d.ts.map +1 -1
- package/dist/types/learning/reward-fixture-executor.d.ts +3 -0
- package/dist/types/learning/reward-fixture-executor.d.ts.map +1 -1
- package/dist/types/learning/schedule-contracts.d.ts +5 -1
- package/dist/types/learning/schedule-contracts.d.ts.map +1 -1
- package/dist/types/learning/schedule-service.d.ts +4 -0
- package/dist/types/learning/schedule-service.d.ts.map +1 -1
- package/dist/types/learning/schedule-worker.d.ts.map +1 -1
- package/dist/types/learning/service.d.ts +10 -0
- package/dist/types/learning/service.d.ts.map +1 -1
- package/dist/types/learning/task-queue-inspection.d.ts +25 -0
- package/dist/types/learning/task-queue-inspection.d.ts.map +1 -0
- package/dist/types/learning/transport.d.ts +224 -5
- package/dist/types/learning/transport.d.ts.map +1 -1
- package/dist/types/rewards.d.ts +107 -0
- package/dist/types/rewards.d.ts.map +1 -1
- package/dist/types/rollouts.d.ts.map +1 -1
- package/package.json +1 -1
- package/schemas/learning/v1/command-request.schema.json +266 -169
- package/schemas/learning/v1/decision.schema.json +192 -0
- package/schemas/learning/v1/grade.schema.json +335 -3
- package/schemas/learning/v1/policy.schema.json +24 -0
- package/schemas/learning/v1/read-request.schema.json +28 -0
- package/schemas/learning/v1/reservation.schema.json +17 -0
- package/schemas/learning/v1/reward.schema.json +22 -0
- package/schemas/learning/v1/reward_check.schema.json +335 -3
- package/schemas/learning/v1/schedule.schema.json +44 -13
- package/schemas/learning/v1/task-queue-inspection.schema.json +72 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { contentHash } from "@openpond/harness";
|
|
2
|
+
import { createRewardRelease } from "../rewards.js";
|
|
3
|
+
import { learningRef, sameLearningRef } from "./contracts.js";
|
|
4
|
+
import { LearningDomainError } from "./errors.js";
|
|
5
|
+
import { requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
6
|
+
import { compileRewardCheck, matchRewardFixture } from "./reward-checks.js";
|
|
7
|
+
/** Qualification is tied to the exact private draft, fixtures and worker-owned
|
|
8
|
+
* provider receipts. Publishing does not rewrite or re-run those checks. */
|
|
9
|
+
export function qualifyRewardCheck(draft, base, check) {
|
|
10
|
+
const compiled = compileRewardCheck(draft, base);
|
|
11
|
+
const implementation = compiled.reward.implementation;
|
|
12
|
+
if (implementation.kind !== "model_judge" || !implementation.model)
|
|
13
|
+
throw new LearningDomainError("reward_calibration_model_required", 422);
|
|
14
|
+
if (!sameLearningRef(check.draft, learningRef(draft)) || !sameLearningRef(check.reward, learningRef(compiled.reward))
|
|
15
|
+
|| check.snapshotHash !== compiled.snapshotHash || contentHash(check.fixtureRefs) !== contentHash(compiled.fixtureRefs))
|
|
16
|
+
throw new LearningDomainError("reward_calibration_snapshot_mismatch", 422);
|
|
17
|
+
if (check.status !== "completed" || !check.runtime || check.matchesExpectations !== true || check.failure
|
|
18
|
+
|| check.results.length !== compiled.fixtures.length)
|
|
19
|
+
throw new LearningDomainError("reward_calibration_check_incomplete", 422);
|
|
20
|
+
if (!compiled.fixtures.some(fixture => fixture.expected.status === "scored" && fixture.expected.passed === true)
|
|
21
|
+
|| !compiled.fixtures.some(fixture => fixture.expected.status === "scored" && fixture.expected.passed === false)) {
|
|
22
|
+
throw new LearningDomainError("reward_calibration_coverage_required", 422, "Check both a passing and a failing example before publishing a calibrated judge.");
|
|
23
|
+
}
|
|
24
|
+
for (const fixture of compiled.fixtures) {
|
|
25
|
+
const results = check.results.filter(result => result.fixture.id === fixture.id);
|
|
26
|
+
const retained = results[0];
|
|
27
|
+
if (results.length !== 1 || !retained || retained.fixture.contentHash !== contentHash(fixture)
|
|
28
|
+
|| !sameLearningRef(retained.result.reward, check.reward) || !matchRewardFixture(fixture, retained.result).matchesExpectation)
|
|
29
|
+
throw new LearningDomainError("reward_calibration_fixture_mismatch", 422);
|
|
30
|
+
if (fixture.expected.status !== "scored")
|
|
31
|
+
continue;
|
|
32
|
+
const receipt = retained.result.graderEvidence?.modelJudgeReceipt;
|
|
33
|
+
const call = check.judgeCalls?.find(call => call.id === `fixture-${contentHash([fixture, compiled.reward.contentHash])}`);
|
|
34
|
+
if (!receipt || receipt.providerId !== implementation.model.providerId || receipt.modelId !== implementation.model.modelId
|
|
35
|
+
|| (implementation.model.revision !== null && receipt.modelRevision !== implementation.model.revision)
|
|
36
|
+
|| call?.status !== "settled" || !call.response || call.requestHash !== receipt.requestHash
|
|
37
|
+
|| contentHash(call.response) !== receipt.responseHash)
|
|
38
|
+
throw new LearningDomainError("reward_calibration_provider_receipt_mismatch", 422);
|
|
39
|
+
}
|
|
40
|
+
const { contentHash: _hash, ...content } = compiled.reward;
|
|
41
|
+
return { ...compiled, reward: createRewardRelease({ ...content, implementation: { ...implementation, calibrationStatus: "passed" },
|
|
42
|
+
calibrationCheckRef: { id: check.id, revision: check.revision, contentHash: contentHash(check) } }) };
|
|
43
|
+
}
|
|
44
|
+
/** Generic publication cannot manufacture a passed flag, including by copying
|
|
45
|
+
* a check from a different rubric, model, temperature or fixture set. */
|
|
46
|
+
export async function assertRewardCalibration(tx, reward) {
|
|
47
|
+
if (reward.implementation.kind !== "model_judge" || reward.implementation.calibrationStatus !== "passed") {
|
|
48
|
+
if (reward.calibrationCheckRef)
|
|
49
|
+
throw new LearningDomainError("reward_calibration_reference_invalid", 422);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const reference = reward.calibrationCheckRef;
|
|
53
|
+
if (!reference)
|
|
54
|
+
throw new LearningDomainError("reward_calibration_check_required", 422);
|
|
55
|
+
const check = await requireLearningResource(tx, "reward_check", reference.id, reference.revision);
|
|
56
|
+
if (contentHash(check) !== reference.contentHash)
|
|
57
|
+
throw new LearningDomainError("reward_calibration_check_changed", 422);
|
|
58
|
+
const draft = await requireLearningRelease(tx, "draft", check.draft);
|
|
59
|
+
if (draft.targetKind !== "reward")
|
|
60
|
+
throw new LearningDomainError("reward_calibration_draft_kind_invalid", 422);
|
|
61
|
+
const base = draft.baseRelease ? await requireLearningRelease(tx, "reward", draft.baseRelease) : null;
|
|
62
|
+
const qualified = qualifyRewardCheck(draft, base, check).reward;
|
|
63
|
+
const scoring = (value) => contentHash([value.implementation, value.rawScore, value.assets, value.fixtureSetRef ?? null]);
|
|
64
|
+
if (reward.id !== qualified.id || scoring(reward) !== scoring(qualified))
|
|
65
|
+
throw new LearningDomainError("reward_calibration_configuration_changed", 422);
|
|
66
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { contentHash } from "@openpond/harness";
|
|
2
|
+
import { requireLearningResource } from "./repository.js";
|
|
3
|
+
import { RewardCheckRunSchema } from "./reward-checks.js";
|
|
4
|
+
/** The same persisted run owns fixture results and budget reservations. A late
|
|
5
|
+
* provider settlement is retained even after its worker loses the lease. */
|
|
6
|
+
export function createRewardCheckJudgeBudgetStore(input) {
|
|
7
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
8
|
+
return {
|
|
9
|
+
transaction(intent, update) {
|
|
10
|
+
return input.repository.transaction(input.scope, async (tx) => {
|
|
11
|
+
const current = await requireLearningResource(tx, "reward_check", input.run.id);
|
|
12
|
+
if (current.snapshotHash !== input.run.snapshotHash)
|
|
13
|
+
throw new Error("model_judge_budget_snapshot_changed");
|
|
14
|
+
if (intent === "dispatch" && (current.status !== "running" || !current.leaseOwner || current.leaseOwner !== input.run.leaseOwner
|
|
15
|
+
|| current.attemptCount !== input.run.attemptCount || !current.leaseExpiresAt || Date.parse(current.leaseExpiresAt) <= Date.parse(now())))
|
|
16
|
+
throw new Error("model_judge_budget_owner_inactive");
|
|
17
|
+
const calls = current.judgeCalls ?? [];
|
|
18
|
+
const next = update({ maximumSpendUsd: current.maximumSpendUsd, calls });
|
|
19
|
+
if (contentHash(next.calls) !== contentHash(calls)) {
|
|
20
|
+
const updated = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1, judgeCalls: next.calls, updatedAt: now() });
|
|
21
|
+
await tx.put("reward_check", updated, current.revision, { parentId: current.reward.id, status: current.status });
|
|
22
|
+
}
|
|
23
|
+
return next.result;
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -3,7 +3,7 @@ import { currentDraft } from "./authoring-service.js";
|
|
|
3
3
|
import { learningRef } from "./contracts.js";
|
|
4
4
|
import { compileRewardCheck, RewardCheckRunSchema } from "./reward-checks.js";
|
|
5
5
|
import { LearningConflictError, requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
6
|
-
export async function queueRewardCheck(tx, input, operationId, now) {
|
|
6
|
+
export async function queueRewardCheck(tx, input, operationId, now, requestedBy) {
|
|
7
7
|
const draft = await currentDraft(tx, input.draft);
|
|
8
8
|
if (draft.targetKind !== "reward")
|
|
9
9
|
throw new LearningDomainError("reward_check_draft_kind_invalid", 422);
|
|
@@ -13,7 +13,7 @@ export async function queueRewardCheck(tx, input, operationId, now) {
|
|
|
13
13
|
schemaVersion: "openpond.rewardCheckRun.v1", id: `reward-check-${operationId}`, revision: 1,
|
|
14
14
|
draft: learningRef(draft), reward: learningRef(compiled.reward), snapshotHash: compiled.snapshotHash,
|
|
15
15
|
fixtureRefs: compiled.fixtureRefs, status: "queued", runtime: null, results: [], matchesExpectations: null,
|
|
16
|
-
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd,
|
|
16
|
+
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, requestedBy,
|
|
17
17
|
leaseOwner: null, leaseExpiresAt: null, attemptCount: 0, failure: null, createdAt: now, updatedAt: now,
|
|
18
18
|
});
|
|
19
19
|
await tx.put("reward_check", check, 0, { parentId: draft.targetId, status: check.status });
|
|
@@ -17,7 +17,7 @@ export function createRewardCheckWorker(repository, executor, options) {
|
|
|
17
17
|
const claimed = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1,
|
|
18
18
|
status: current.status === "cancelling" ? "cancelling" : "running", runtime: current.status === "cancelling" ? current.runtime : runtime,
|
|
19
19
|
leaseOwner: options.workerId, leaseExpiresAt: new Date(Date.parse(now()) + current.timeoutMs + 30_000).toISOString(),
|
|
20
|
-
attemptCount: current.attemptCount + 1, results: current.
|
|
20
|
+
attemptCount: current.attemptCount + 1, results: current.results, matchesExpectations: null, failure: null, updatedAt: now(),
|
|
21
21
|
});
|
|
22
22
|
await tx.put("reward_check", claimed, current.revision, { parentId: claimed.reward.id, status: claimed.status });
|
|
23
23
|
return { acquired: true, run: claimed };
|
|
@@ -44,8 +44,16 @@ export function createRewardCheckWorker(repository, executor, options) {
|
|
|
44
44
|
});
|
|
45
45
|
for (const fixture of compiled.fixtures) {
|
|
46
46
|
controller.signal.throwIfAborted();
|
|
47
|
+
const previous = claim.run.results.find(result => result.fixture.id === fixture.id);
|
|
48
|
+
if (previous) {
|
|
49
|
+
if (previous.fixture.contentHash !== contentHash(fixture) || !sameLearningRef(previous.result.reward, claim.run.reward)
|
|
50
|
+
|| previous.result.graderId !== compiled.reward.id || previous.result.role !== "evaluation")
|
|
51
|
+
throw new Error("reward_check_retained_fixture_mismatch");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
47
54
|
const result = BoundRewardResultSchema.parse(await executor.execute({ ...compiled, scope, run: claim.run, fixture, signal: controller.signal }));
|
|
48
|
-
|
|
55
|
+
// Retain a settled execution before observing cancellation. Its provider
|
|
56
|
+
// cost and evidence already exist even if no more fixtures should run.
|
|
49
57
|
if (result.graderId !== compiled.reward.id || !sameLearningRef(result.reward, claim.run.reward) || result.role !== "evaluation")
|
|
50
58
|
throw new Error("reward_check_result_identity_mismatch");
|
|
51
59
|
const retained = await repository.transaction(scope, async (tx) => {
|
|
@@ -5,6 +5,7 @@ import { TaskRecordSchema } from "../tasksets.js";
|
|
|
5
5
|
import { LearningJsonObjectSchema, LearningRevisionRefSchema, learningRef } from "./contracts.js";
|
|
6
6
|
import { authoringNumber, compileRewardAuthoring, parseRewardAuthoringObject } from "./reward-authoring.js";
|
|
7
7
|
import { LearningDomainError } from "./errors.js";
|
|
8
|
+
import { JudgeCallReservationSchema } from "./judge-budget.js";
|
|
8
9
|
export const RewardFixtureSchema = z.object({
|
|
9
10
|
id: ReleaseIdSchema, name: z.string().trim().min(1).max(500),
|
|
10
11
|
input: LearningJsonObjectSchema, output: LearningJsonObjectSchema,
|
|
@@ -32,6 +33,8 @@ export const RewardCheckRunSchema = z.object({
|
|
|
32
33
|
runtime: RewardCheckRuntimeSchema.nullable(), results: z.array(RewardFixtureCheckResultSchema).max(50),
|
|
33
34
|
matchesExpectations: z.boolean().nullable(), failure: z.string().max(20_000).nullable(),
|
|
34
35
|
timeoutMs: z.number().int().min(100).max(300_000), maximumSpendUsd: z.number().nonnegative().max(1_000),
|
|
36
|
+
judgeCalls: z.array(JudgeCallReservationSchema).max(1_000).optional(),
|
|
37
|
+
requestedBy: ReleaseIdSchema.optional(),
|
|
35
38
|
leaseOwner: ReleaseIdSchema.nullable(), leaseExpiresAt: ReleaseTimestampSchema.nullable(), attemptCount: z.number().int().nonnegative(),
|
|
36
39
|
createdAt: ReleaseTimestampSchema, updatedAt: ReleaseTimestampSchema,
|
|
37
40
|
}).strict();
|
|
@@ -69,7 +72,7 @@ export async function executeRewardFixture(input) {
|
|
|
69
72
|
}, [input.reward]);
|
|
70
73
|
const composition = await executeRewardBinding({ binding, rewards: [input.reward], task,
|
|
71
74
|
evidence: { output: fixture.output, artifactRefs: fixture.artifactRefs, runtimeEventRefs: fixture.runtimeEventRefs, infrastructureError: fixture.infrastructureError },
|
|
72
|
-
signal: input.signal, customVerifier: input.customVerifier, modelJudge: input.modelJudge,
|
|
75
|
+
signal: input.signal, customVerifier: input.customVerifier, modelJudge: input.modelJudge, purpose: "fixture_calibration",
|
|
73
76
|
});
|
|
74
77
|
return composition.results[0];
|
|
75
78
|
}
|
|
@@ -4,9 +4,10 @@ import { executeRewardFixture } from "./reward-checks.js";
|
|
|
4
4
|
export function createIsolatedRewardFixtureExecutor(options) {
|
|
5
5
|
return {
|
|
6
6
|
runtime: options.runtime,
|
|
7
|
-
execute(input) {
|
|
7
|
+
async execute(input) {
|
|
8
8
|
return executeRewardFixture({
|
|
9
9
|
reward: input.reward, fixture: input.fixture, signal: input.signal,
|
|
10
|
+
modelJudge: input.reward.implementation.kind === "model_judge" ? await options.createModelJudge?.(input) : undefined,
|
|
10
11
|
customVerifier: async ({ grader, task, evidence }) => {
|
|
11
12
|
const asset = input.assets.find(asset => asset.id === grader.verifierRef.id);
|
|
12
13
|
if (!asset)
|
|
@@ -24,6 +25,6 @@ export function createIsolatedRewardFixtureExecutor(options) {
|
|
|
24
25
|
});
|
|
25
26
|
},
|
|
26
27
|
// No remote allocation; executeJavaScript's settlement contract confirms cleanup.
|
|
27
|
-
async cancel() { return true; },
|
|
28
|
+
async cancel(input) { return options.createModelJudge ? options.cancelModelJudge?.(input) ?? false : true; },
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
|
|
3
3
|
import { LearningRevisionRefSchema } from "./contracts.js";
|
|
4
|
+
import { NightlyScheduleSchema } from "./nightly-schedule.js";
|
|
4
5
|
/** The durable timer is operational state; every fire retains its own policy. */
|
|
5
6
|
export const LearningScheduleSchema = z.object({
|
|
6
7
|
schemaVersion: z.literal("openpond.learningSchedule.v1"),
|
|
@@ -11,6 +12,7 @@ export const LearningScheduleSchema = z.object({
|
|
|
11
12
|
executionOwner: z.enum(["local", "hosted"]),
|
|
12
13
|
state: z.enum(["scheduled", "disabled", "blocked"]),
|
|
13
14
|
intervalSeconds: z.number().int().min(60).max(31_536_000).nullable(),
|
|
15
|
+
calendar: NightlyScheduleSchema.nullable().default(null),
|
|
14
16
|
nextRunAt: ReleaseTimestampSchema.nullable(),
|
|
15
17
|
lastFire: LearningRevisionRefSchema.nullable(),
|
|
16
18
|
consecutiveFailures: z.number().int().nonnegative(),
|
|
@@ -19,10 +21,12 @@ export const LearningScheduleSchema = z.object({
|
|
|
19
21
|
createdAt: ReleaseTimestampSchema,
|
|
20
22
|
updatedAt: ReleaseTimestampSchema,
|
|
21
23
|
}).strict().superRefine((value, context) => {
|
|
22
|
-
if (value.state !== "disabled" && (!value.intervalSeconds || !value.nextRunAt))
|
|
24
|
+
if (value.state !== "disabled" && ((!value.intervalSeconds && !value.calendar) || !value.nextRunAt))
|
|
23
25
|
context.addIssue({ code: "custom", path: ["nextRunAt"], message: "An active timer must retain its cadence and due time." });
|
|
24
26
|
if (value.state === "disabled" && value.nextRunAt)
|
|
25
27
|
context.addIssue({ code: "custom", path: ["nextRunAt"], message: "A disabled timer cannot have a pending occurrence." });
|
|
28
|
+
if (value.intervalSeconds && value.calendar)
|
|
29
|
+
context.addIssue({ code: "custom", path: ["calendar"], message: "A timer uses either an interval or a nightly calendar." });
|
|
26
30
|
});
|
|
27
31
|
/** Immutable occurrence evidence survives timer edits and coalesced downtime. */
|
|
28
32
|
export const LearningScheduleFireSchema = z.object({
|
|
@@ -2,7 +2,8 @@ import { contentHash } from "@openpond/harness";
|
|
|
2
2
|
import { assertLearningContentHash, learningRef, sameLearningRef } from "./contracts.js";
|
|
3
3
|
import { LearningScheduleSchema } from "./schedule-contracts.js";
|
|
4
4
|
import { LearningDomainError } from "./errors.js";
|
|
5
|
-
import { requireLearningResource } from "./repository.js";
|
|
5
|
+
import { requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
6
|
+
import { nextNightlyOccurrence } from "./nightly-schedule.js";
|
|
6
7
|
export function learningScheduleId(policyId) { return `schedule-${contentHash(policyId)}`; }
|
|
7
8
|
/** Called atomically with policy publication. No external timer or Work is made. */
|
|
8
9
|
export async function synchronizeLearningSchedule(tx, policy, now) {
|
|
@@ -14,15 +15,18 @@ export async function synchronizeLearningSchedule(tx, policy, now) {
|
|
|
14
15
|
const previous = await tx.get("schedule", id);
|
|
15
16
|
if (previous && sameLearningRef(previous.policy, learningRef(policy)))
|
|
16
17
|
return previous;
|
|
17
|
-
const intervalSeconds = policy.trigger.kind === "schedule" ? policy.trigger.intervalSeconds : null;
|
|
18
|
-
const
|
|
18
|
+
const intervalSeconds = policy.trigger.kind === "schedule" ? policy.trigger.intervalSeconds : policy.trigger.kind === "approved_count" ? 60 : null;
|
|
19
|
+
const calendar = policy.trigger.kind === "nightly" ? { localTime: policy.trigger.localTime, timeZone: policy.trigger.timeZone } : null;
|
|
20
|
+
const enabled = policy.enabled && policy.automation.train && (intervalSeconds !== null || calendar !== null);
|
|
21
|
+
const previousPolicy = previous ? await requireLearningRelease(tx, "policy", previous.policy) : null;
|
|
19
22
|
const preserveDue = previous?.nextRunAt && previous.intervalSeconds === intervalSeconds
|
|
23
|
+
&& JSON.stringify(previous.calendar) === JSON.stringify(calendar) && previousPolicy?.trigger.kind === policy.trigger.kind
|
|
20
24
|
&& previous.executionOwner === policy.executionOwner;
|
|
21
25
|
const schedule = LearningScheduleSchema.parse({
|
|
22
26
|
schemaVersion: "openpond.learningSchedule.v1", id, revision: (previous?.revision ?? 0) + 1,
|
|
23
27
|
policy: learningRef(policy), modelProjectId: policy.modelProjectId, executionOwner: policy.executionOwner,
|
|
24
|
-
state: enabled ? "scheduled" : "disabled", intervalSeconds,
|
|
25
|
-
nextRunAt: enabled ? preserveDue ? previous.nextRunAt : new Date(Date.parse(now) + intervalSeconds * 1_000).toISOString() : null,
|
|
28
|
+
state: enabled ? "scheduled" : "disabled", intervalSeconds, calendar,
|
|
29
|
+
nextRunAt: enabled ? preserveDue ? previous.nextRunAt : calendar ? nextNightlyOccurrence(calendar, now) : new Date(Date.parse(now) + intervalSeconds * 1_000).toISOString() : null,
|
|
26
30
|
lastFire: previous?.lastFire ?? null, consecutiveFailures: 0, nextAttemptAt: null, lastError: null,
|
|
27
31
|
createdAt: previous?.createdAt ?? now, updatedAt: now,
|
|
28
32
|
});
|
|
@@ -4,6 +4,8 @@ import { LearningDomainError } from "./errors.js";
|
|
|
4
4
|
import { reserveLearningIteration } from "./iteration-reservation-service.js";
|
|
5
5
|
import { LearningScheduleFireSchema, LearningScheduleSchema } from "./schedule-contracts.js";
|
|
6
6
|
import { requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
7
|
+
import { inspectIterationEligibility } from "./iteration-eligibility.js";
|
|
8
|
+
import { coalesceNightlyOccurrences } from "./nightly-schedule.js";
|
|
7
9
|
const skipReasons = new Map([
|
|
8
10
|
["learning_iteration_active", "active_iteration"], ["learning_iteration_cooldown", "cooldown"],
|
|
9
11
|
["learning_daily_budget_exhausted", "daily_budget"],
|
|
@@ -28,20 +30,33 @@ export function createLearningScheduleWorker(repository, options) {
|
|
|
28
30
|
attempt = { schedule, maxRetries: 0 };
|
|
29
31
|
const policy = await requireLearningRelease(tx, "policy", schedule.policy);
|
|
30
32
|
attempt.maxRetries = policy.limits.maxRetries;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const matches = policy.trigger.kind === "nightly" ? schedule.intervalSeconds === null && schedule.calendar?.localTime === policy.trigger.localTime && schedule.calendar.timeZone === policy.trigger.timeZone
|
|
34
|
+
: policy.trigger.kind === "schedule" ? schedule.calendar === null && policy.trigger.intervalSeconds === schedule.intervalSeconds
|
|
35
|
+
: policy.trigger.kind === "approved_count" && schedule.calendar === null && schedule.intervalSeconds === 60;
|
|
36
|
+
if (policy.executionOwner !== schedule.executionOwner || !matches)
|
|
33
37
|
throw new LearningDomainError("learning_schedule_policy_mismatch", 409);
|
|
34
38
|
const scheduledAt = schedule.nextRunAt;
|
|
35
|
-
const interval = schedule.intervalSeconds * 1_000;
|
|
39
|
+
const interval = (schedule.intervalSeconds ?? 86_400) * 1_000;
|
|
36
40
|
const due = Date.parse(scheduledAt);
|
|
37
41
|
const coalescedCount = Math.floor((time - due) / interval);
|
|
42
|
+
const occurrence = schedule.calendar ? coalesceNightlyOccurrences(schedule.calendar, scheduledAt, timestamp) : {
|
|
43
|
+
coalescedCount, coalescedThroughAt: new Date(due + coalescedCount * interval).toISOString(), nextRunAt: new Date(due + (coalescedCount + 1) * interval).toISOString(),
|
|
44
|
+
};
|
|
45
|
+
// Below-threshold count checks advance their timer without filling the
|
|
46
|
+
// Model's history with empty iterations or consuming any evidence.
|
|
47
|
+
if (policy.trigger.kind === "approved_count" && (await inspectIterationEligibility(tx, policy)).counts.eligible < policy.admission.minimumApprovedExamples) {
|
|
48
|
+
const updated = LearningScheduleSchema.parse({ ...schedule, revision: schedule.revision + 1, nextRunAt: occurrence.nextRunAt,
|
|
49
|
+
consecutiveFailures: 0, nextAttemptAt: null, lastError: null, updatedAt: timestamp });
|
|
50
|
+
await tx.put("schedule", updated, schedule.revision, { parentId: schedule.modelProjectId, status: updated.state });
|
|
51
|
+
return updated;
|
|
52
|
+
}
|
|
38
53
|
const id = `schedule-fire-${contentHash([schedule.id, scheduledAt])}`;
|
|
39
54
|
let outcome = "skipped";
|
|
40
55
|
let reason = null;
|
|
41
56
|
let iteration = null;
|
|
42
57
|
try {
|
|
43
58
|
const pointers = await reserveLearningIteration(tx, { action: "reserve_iteration", operationId: id,
|
|
44
|
-
policy: schedule.policy, trigger: { kind: "schedule", scheduledAt } }, actorId, timestamp);
|
|
59
|
+
policy: schedule.policy, trigger: policy.trigger.kind === "approved_count" ? { kind: "approved_count", checkedAt: scheduledAt } : { kind: "schedule", scheduledAt } }, actorId, timestamp);
|
|
45
60
|
const pointer = pointers.find(value => value.kind === "iteration");
|
|
46
61
|
if (!pointer)
|
|
47
62
|
throw new LearningDomainError("learning_schedule_reservation_missing", 409);
|
|
@@ -59,12 +74,12 @@ export function createLearningScheduleWorker(repository, options) {
|
|
|
59
74
|
}
|
|
60
75
|
const fire = LearningScheduleFireSchema.parse(sealLearningContent({
|
|
61
76
|
schemaVersion: "openpond.learningScheduleFire.v1", id, revision: 1, scheduleId,
|
|
62
|
-
policy: schedule.policy, scheduledAt, coalescedThroughAt:
|
|
63
|
-
coalescedCount, outcome, reason, iteration, createdAt: timestamp,
|
|
77
|
+
policy: schedule.policy, scheduledAt, coalescedThroughAt: occurrence.coalescedThroughAt,
|
|
78
|
+
coalescedCount: occurrence.coalescedCount, outcome, reason, iteration, createdAt: timestamp,
|
|
64
79
|
}));
|
|
65
80
|
await tx.put("schedule_fire", fire, 0, { parentId: schedule.id, status: outcome });
|
|
66
81
|
const updated = LearningScheduleSchema.parse({ ...schedule, revision: schedule.revision + 1,
|
|
67
|
-
nextRunAt:
|
|
82
|
+
nextRunAt: occurrence.nextRunAt, lastFire: learningRef(fire),
|
|
68
83
|
consecutiveFailures: 0, nextAttemptAt: null, lastError: null, updatedAt: timestamp });
|
|
69
84
|
await tx.put("schedule", updated, schedule.revision, { parentId: schedule.modelProjectId, status: updated.state });
|
|
70
85
|
return updated;
|
package/dist/learning/service.js
CHANGED
|
@@ -4,10 +4,13 @@ import { reserveLearningIteration } from "./iteration-reservation-service.js";
|
|
|
4
4
|
import { synchronizeLearningSchedule } from "./schedule-service.js";
|
|
5
5
|
import { inspectLearningPolicy } from "./policy-inspection.js";
|
|
6
6
|
import { requireCurrentLearningEvidence as currentEvidence, sealLearningBatch } from "./batch-service.js";
|
|
7
|
-
import { saveAuthoringDraft, archiveAuthoringDraft, finalizeAuthoringDraft } from "./authoring-service.js";
|
|
7
|
+
import { saveAuthoringDraft, archiveAuthoringDraft, finalizeAuthoringDraft, currentDraft } from "./authoring-service.js";
|
|
8
|
+
import { assertRewardCalibration, qualifyRewardCheck } from "./reward-calibration.js";
|
|
8
9
|
import { queueRewardCheck, cancelRewardCheck } from "./reward-check-service.js";
|
|
9
10
|
import { LearningDomainError } from "./errors.js";
|
|
10
11
|
import { contentHash } from "@openpond/harness";
|
|
12
|
+
import { inspectLearningTaskQueue } from "./task-queue-inspection.js";
|
|
13
|
+
import { LearningReadRequestSchema } from "./transport.js";
|
|
11
14
|
import { createRewardBinding, createRewardRelease } from "../rewards.js";
|
|
12
15
|
import { assertBoundedTaskJson, validateTaskValue } from "../task-schema.js";
|
|
13
16
|
import { assertAdmissionDecision, assertGradeIdentity, inspectTaskEvidence } from "./admission.js";
|
|
@@ -36,6 +39,29 @@ export function createLearningService(repository, options = {}) {
|
|
|
36
39
|
}
|
|
37
40
|
let pointers;
|
|
38
41
|
switch (input.action) {
|
|
42
|
+
case "publish_checked_reward": {
|
|
43
|
+
const draft = await currentDraft(transaction, input.draft);
|
|
44
|
+
if (draft.targetKind !== "reward")
|
|
45
|
+
throw new LearningDomainError("reward_calibration_draft_kind_invalid", 422);
|
|
46
|
+
const base = draft.baseRelease ? await requireLearningRelease(transaction, "reward", draft.baseRelease) : null;
|
|
47
|
+
const check = await requireLearningResource(transaction, "reward_check", input.checkId, input.checkRevision);
|
|
48
|
+
const qualified = qualifyRewardCheck(draft, base, check);
|
|
49
|
+
const { contentHash: _rewardHash, ...content } = qualified.reward;
|
|
50
|
+
const publication = { action: "publish_resources", operationId: input.operationId,
|
|
51
|
+
finalizeDraft: { draft: input.draft, targetKind: "reward", release: learningRef(qualified.reward) },
|
|
52
|
+
resources: [
|
|
53
|
+
...qualified.assets.map(({ contentHash: _assetHash, ...content }) => ({ kind: "asset", expectedRevision: 0, content })),
|
|
54
|
+
{ kind: "reward", expectedRevision: base?.revision ?? 0, content },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
pointers = [];
|
|
58
|
+
for (const resource of publication.resources)
|
|
59
|
+
pointers.push(await publish(transaction, { ...resource, action: "publish", operationId: input.operationId }));
|
|
60
|
+
const finalized = await finalizeAuthoringDraft(transaction, publication, pointers, now());
|
|
61
|
+
if (finalized)
|
|
62
|
+
pointers.push(finalized);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
39
65
|
case "import_intake":
|
|
40
66
|
pointers = await importTaskIntake(transaction, input, now(), submit);
|
|
41
67
|
break;
|
|
@@ -50,7 +76,7 @@ export function createLearningService(repository, options = {}) {
|
|
|
50
76
|
pointers = await reserveLearningIteration(transaction, input, context.actor.id, now());
|
|
51
77
|
break;
|
|
52
78
|
case "queue_reward_check":
|
|
53
|
-
pointers = [await queueRewardCheck(transaction, input, operationId, now())];
|
|
79
|
+
pointers = [await queueRewardCheck(transaction, input, operationId, now(), context.actor.id)];
|
|
54
80
|
break;
|
|
55
81
|
case "cancel_reward_check":
|
|
56
82
|
pointers = [await cancelRewardCheck(transaction, input, now())];
|
|
@@ -83,7 +109,7 @@ export function createLearningService(repository, options = {}) {
|
|
|
83
109
|
pointers = [await resolveFeedback(transaction, input, context.actor.id)];
|
|
84
110
|
break;
|
|
85
111
|
case "queue_grade":
|
|
86
|
-
pointers = [await queueGrade(transaction, input, operationId)];
|
|
112
|
+
pointers = [await queueGrade(transaction, input, operationId, context.actor.id)];
|
|
87
113
|
break;
|
|
88
114
|
case "cancel_grade":
|
|
89
115
|
pointers = [await cancelGrade(transaction, input)];
|
|
@@ -118,6 +144,7 @@ export function createLearningService(repository, options = {}) {
|
|
|
118
144
|
}
|
|
119
145
|
case "reward": {
|
|
120
146
|
resource = createRewardRelease(input.content);
|
|
147
|
+
await assertRewardCalibration(transaction, resource);
|
|
121
148
|
const implementation = input.content.implementation;
|
|
122
149
|
const references = [
|
|
123
150
|
...input.content.assets,
|
|
@@ -282,7 +309,7 @@ export function createLearningService(repository, options = {}) {
|
|
|
282
309
|
await transaction.put("feedback", resolved, feedback.revision, { parentId: learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId), status: resolved.status });
|
|
283
310
|
return pointer("feedback", resolved);
|
|
284
311
|
}
|
|
285
|
-
async function queueGrade(transaction, input, operationId) {
|
|
312
|
+
async function queueGrade(transaction, input, operationId, requestedBy) {
|
|
286
313
|
const evidence = await currentEvidence(transaction, input.evidence);
|
|
287
314
|
const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
|
|
288
315
|
if (!inspectTaskEvidence(evidence, definition).taskReady)
|
|
@@ -296,7 +323,7 @@ export function createLearningService(repository, options = {}) {
|
|
|
296
323
|
schemaVersion: "openpond.taskGradeRun.v1", id: `grade-${operationId}`, revision: 1,
|
|
297
324
|
evidence: learningRef(evidence), binding: definition.rewardBinding, target: input.target, output,
|
|
298
325
|
status: "queued", composition: null, leaseOwner: null, leaseExpiresAt: null, attemptCount: 0,
|
|
299
|
-
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, failure: null, createdAt: now(), updatedAt: now(),
|
|
326
|
+
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, requestedBy, failure: null, createdAt: now(), updatedAt: now(),
|
|
300
327
|
});
|
|
301
328
|
await transaction.put("grade", grade, 0, { parentId: evidence.id, status: grade.status });
|
|
302
329
|
return pointer("grade", grade);
|
|
@@ -335,6 +362,13 @@ export function createLearningService(repository, options = {}) {
|
|
|
335
362
|
}
|
|
336
363
|
return {
|
|
337
364
|
command,
|
|
365
|
+
async inspectTaskQueue(context, modelProjectId = null) {
|
|
366
|
+
authorizeRead(context);
|
|
367
|
+
const input = LearningReadRequestSchema.parse({ action: "inspect_task_queue", scope: context.scope, modelProjectId });
|
|
368
|
+
if (input.action !== "inspect_task_queue")
|
|
369
|
+
throw new LearningDomainError("learning_request_invalid", 400);
|
|
370
|
+
return repository.transaction(context.scope, transaction => inspectLearningTaskQueue(transaction, input.modelProjectId, now()));
|
|
371
|
+
},
|
|
338
372
|
async inspectPolicy(context, reference) {
|
|
339
373
|
authorizeRead(context);
|
|
340
374
|
const ref = LearningRevisionRefSchema.parse(reference);
|
|
@@ -382,7 +416,7 @@ function authorize(context, input) {
|
|
|
382
416
|
throw new LearningDomainError("learning_scope_required", 400);
|
|
383
417
|
const publications = input.action === "publish" ? [input] : input.action === "publish_resources" ? input.resources : [];
|
|
384
418
|
if (context.actor.role !== "reviewer" && publications.some(value => value.kind === "policy" && value.content.enabled
|
|
385
|
-
&& value.content.automation.train && value.content.trigger.kind
|
|
419
|
+
&& value.content.automation.train && value.content.trigger.kind !== "manual"))
|
|
386
420
|
throw new LearningDomainError("learning_review_not_authorized", 403);
|
|
387
421
|
if (context.actor.role === "source") {
|
|
388
422
|
const sourceId = input.action === "submit_example" ? input.example.sourceId : input.action === "submit_feedback" ? input.feedback.sourceId : null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { LearningDomainError } from "./errors.js";
|
|
4
|
+
import { inspectIterationEligibility } from "./iteration-eligibility.js";
|
|
5
|
+
import { taskFamilyReservations } from "./admission.js";
|
|
6
|
+
import { requireLearningRelease } from "./repository.js";
|
|
7
|
+
export const LearningTaskQueueInspectionSchema = z.object({
|
|
8
|
+
modelProjectId: ReleaseIdSchema.nullable(),
|
|
9
|
+
inspectedAt: ReleaseTimestampSchema,
|
|
10
|
+
pendingTrainingCount: z.number().int().nonnegative().nullable(),
|
|
11
|
+
issues: z.array(z.object({ policyId: ReleaseIdSchema, code: z.string(), message: z.string() }).strict()),
|
|
12
|
+
}).strict();
|
|
13
|
+
/** Count exact eligible, unconsumed attempts once across selected policies.
|
|
14
|
+
* Paused policies still have pending work; review-only and held-out tasks do not. */
|
|
15
|
+
export async function inspectLearningTaskQueue(transaction, modelProjectId, now) {
|
|
16
|
+
const eligible = new Set();
|
|
17
|
+
const issues = [];
|
|
18
|
+
let afterId;
|
|
19
|
+
do {
|
|
20
|
+
const page = await transaction.list("policy", { limit: 100, ...(modelProjectId ? { parentId: modelProjectId } : {}), ...(afterId ? { afterId } : {}) });
|
|
21
|
+
for (const policy of page.items) {
|
|
22
|
+
try {
|
|
23
|
+
const inspection = await inspectIterationEligibility(transaction, policy, { collectEligible: true });
|
|
24
|
+
const definition = await requireLearningRelease(transaction, "definition", policy.taskDefinition);
|
|
25
|
+
for (const reference of inspection.eligibleEvidence) {
|
|
26
|
+
const evidence = await requireLearningRelease(transaction, "evidence", reference);
|
|
27
|
+
let heldOut = false;
|
|
28
|
+
for (const family of taskFamilyReservations(evidence, definition)) {
|
|
29
|
+
const split = await transaction.familySplit(family.namespace, family.kind, family.key);
|
|
30
|
+
if (split !== null && split !== "train") {
|
|
31
|
+
heldOut = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!heldOut)
|
|
36
|
+
eligible.add(JSON.stringify([reference.id, reference.revision, reference.contentHash]));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!(error instanceof LearningDomainError))
|
|
41
|
+
throw error;
|
|
42
|
+
issues.push({ policyId: policy.id, code: error.code, message: error.message });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
afterId = page.nextCursor ?? undefined;
|
|
46
|
+
} while (afterId);
|
|
47
|
+
return LearningTaskQueueInspectionSchema.parse({ modelProjectId, inspectedAt: now, pendingTrainingCount: issues.length ? null : eligible.size, issues });
|
|
48
|
+
}
|
|
@@ -17,6 +17,7 @@ export function assertLearningRequestJson(raw) {
|
|
|
17
17
|
/** Scope selection is never authorization; each host resolves its authenticated owner. */
|
|
18
18
|
export const LearningCommandRequestSchema = z.object({ scope: ReleaseIdSchema, command: LearningCommandSchema }).strict();
|
|
19
19
|
export const LearningReadRequestSchema = z.discriminatedUnion("action", [
|
|
20
|
+
z.object({ action: z.literal("inspect_task_queue"), scope: ReleaseIdSchema, modelProjectId: ReleaseIdSchema.nullable() }).strict(),
|
|
20
21
|
z.object({ action: z.literal("inspect_policy"), scope: ReleaseIdSchema, policy: LearningRevisionRefSchema }).strict(),
|
|
21
22
|
z.object({ action: z.literal("inspect_evidence"), scope: ReleaseIdSchema, evidence: LearningRevisionRefSchema }).strict(),
|
|
22
23
|
z.object({ action: z.literal("get"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, id: ReleaseIdSchema, revision: z.number().int().positive().optional() }).strict(),
|
package/dist/rewards.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LearningDomainError } from "./learning/errors.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ImmutableAssetRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash } from "@openpond/harness";
|
|
4
|
-
import { gradeEvidence } from "./graders.js";
|
|
4
|
+
import { gradeEvidence, GraderEvidenceSchema } from "./graders.js";
|
|
5
5
|
import { CustomVerifierGraderSpecSchema, DeterministicGraderSpecSchema, GraderSpecSchema, HumanGraderSpecSchema, ModelJudgeGraderSpecSchema } from "./tasksets.js";
|
|
6
6
|
import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema.js";
|
|
7
7
|
const bindingFields = { id: true, version: true, weight: true, hardGate: true, rewardEligible: true, privileged: true };
|
|
@@ -22,6 +22,7 @@ export const RewardReleaseContentSchema = z.object({
|
|
|
22
22
|
rawScore: z.object({ minimum: z.number().finite(), maximum: z.number().finite() }).strict(),
|
|
23
23
|
assets: z.array(ImmutableAssetRefSchema).max(1_000),
|
|
24
24
|
fixtureSetRef: ImmutableAssetRefSchema.optional(),
|
|
25
|
+
calibrationCheckRef: ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).optional(),
|
|
25
26
|
}).strict().superRefine((reward, context) => {
|
|
26
27
|
if (reward.fixtureSetRef && (reward.fixtureSetRef.visibility !== "verifier" || !reward.assets.some(asset => contentHash(asset) === contentHash(reward.fixtureSetRef))))
|
|
27
28
|
context.addIssue({ code: "custom", path: ["fixtureSetRef"], message: "Reward fixtures must be included as a private verifier asset." });
|
|
@@ -86,6 +87,7 @@ export const BoundRewardResultSchema = z.object({
|
|
|
86
87
|
normalizedScore: z.number().min(0).max(1).nullable(),
|
|
87
88
|
passed: z.boolean().nullable(),
|
|
88
89
|
evidenceHashes: z.array(ReleaseHashSchema).max(1_000),
|
|
90
|
+
graderEvidence: GraderEvidenceSchema.optional(),
|
|
89
91
|
message: z.string().max(20_000).nullable(),
|
|
90
92
|
}).strict();
|
|
91
93
|
export const RewardCompositionContentSchema = z.object({
|
|
@@ -136,6 +138,8 @@ export function compileBoundGraders(binding, rewards) {
|
|
|
136
138
|
}
|
|
137
139
|
export async function executeRewardBinding(input) {
|
|
138
140
|
const resolved = resolveBoundRewards(input.binding, input.rewards);
|
|
141
|
+
if (input.purpose === "fixture_calibration" && resolved.some(({ source }) => source.role !== "evaluation"))
|
|
142
|
+
throw new LearningDomainError("reward_calibration_requires_evaluation_binding");
|
|
139
143
|
const results = [];
|
|
140
144
|
for (const { source, reward } of resolved) {
|
|
141
145
|
input.signal?.throwIfAborted();
|
|
@@ -154,10 +158,10 @@ export async function executeRewardBinding(input) {
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
else {
|
|
157
|
-
const [evidence] = await gradeEvidence({ task: input.task, evidence: input.evidence, graders: [compileSource(source, reward)], modelJudge: input.modelJudge, customVerifier: input.customVerifier });
|
|
161
|
+
const [evidence] = await gradeEvidence({ task: input.task, evidence: input.evidence, graders: [compileSource(source, reward)], modelJudge: input.modelJudge, customVerifier: input.customVerifier, purpose: input.purpose, signal: input.signal });
|
|
158
162
|
if (!evidence)
|
|
159
163
|
throw new LearningDomainError("reward_evidence_missing");
|
|
160
|
-
Object.assign(result, { status: evidence.score === null ? "unavailable" : "scored", rawScore: evidence.score, passed: evidence.score === null ? null : evidence.passed, evidenceHashes: [evidence.contentHash], message: evidence.feedback.join("\n") });
|
|
164
|
+
Object.assign(result, { status: evidence.score === null ? "unavailable" : "scored", rawScore: evidence.score, passed: evidence.score === null ? null : evidence.passed, evidenceHashes: [evidence.contentHash], graderEvidence: evidence, message: evidence.feedback.join("\n") });
|
|
161
165
|
}
|
|
162
166
|
if (result.status === "scored") {
|
|
163
167
|
if (result.rawScore === null || !Number.isFinite(result.rawScore) || result.rawScore < reward.rawScore.minimum || result.rawScore > reward.rawScore.maximum)
|
|
@@ -186,6 +190,14 @@ export function composeBoundRewards(input) {
|
|
|
186
190
|
const result = results.find((candidate) => candidate.graderId === source.graderId);
|
|
187
191
|
if (!result || contentHash(result.reward) !== contentHash(source.reward) || result.role !== source.role)
|
|
188
192
|
throw new LearningDomainError("reward_result_binding_mismatch");
|
|
193
|
+
if (result.graderEvidence) {
|
|
194
|
+
const { contentHash: evidenceHash, ...body } = result.graderEvidence;
|
|
195
|
+
if (contentHash(body) !== evidenceHash || !result.evidenceHashes.includes(evidenceHash)
|
|
196
|
+
|| body.graderId !== source.graderId || body.graderVersion !== String(source.reward.revision))
|
|
197
|
+
throw new LearningDomainError("reward_grader_evidence_mismatch");
|
|
198
|
+
if (result.status === "scored" && (body.score !== result.rawScore || body.passed !== result.passed))
|
|
199
|
+
throw new LearningDomainError("reward_grader_evidence_score_mismatch");
|
|
200
|
+
}
|
|
189
201
|
if ((result.status === "scored") !== (result.rawScore !== null && result.normalizedScore !== null && result.passed !== null))
|
|
190
202
|
throw new LearningDomainError("reward_result_score_state_invalid");
|
|
191
203
|
if (result.status !== "scored" && (result.rawScore !== null || result.normalizedScore !== null || result.passed !== null))
|
package/dist/rollouts.js
CHANGED
|
@@ -116,7 +116,7 @@ export const CanonicalRolloutRecordSchema = z.object({
|
|
|
116
116
|
...CanonicalRolloutRecordFieldsSchema.shape,
|
|
117
117
|
contentHash: ReleaseHashSchema,
|
|
118
118
|
}).strict().superRefine(validateCanonicalRolloutRecord);
|
|
119
|
-
export const RolloutQualificationSchema = z.object({
|
|
119
|
+
export const RolloutQualificationSchema = /* @__PURE__ */ (() => z.object({
|
|
120
120
|
schemaVersion: z.literal("openpond.rolloutQualification.v1"),
|
|
121
121
|
rolloutCount: z.number().int().nonnegative(),
|
|
122
122
|
scoredCount: z.number().int().nonnegative(),
|
|
@@ -128,7 +128,7 @@ export const RolloutQualificationSchema = z.object({
|
|
|
128
128
|
distinctRewardCount: z.number().int().nonnegative(),
|
|
129
129
|
eligibleForRl: z.boolean(),
|
|
130
130
|
reasons: z.array(z.string().trim().min(1).max(1_000)).max(100),
|
|
131
|
-
}).strict();
|
|
131
|
+
}).strict())();
|
|
132
132
|
export function createCanonicalRolloutRecord(input) {
|
|
133
133
|
const attemptReceipt = AttemptReceiptSchema.parse(input.attemptReceipt);
|
|
134
134
|
if (!verifyAttemptReceipt(attemptReceipt)) {
|