@openpond/evals 0.11.10 → 0.11.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +20 -0
- package/dist/experiments.js +243 -0
- package/dist/index.js +1 -0
- package/dist/learning/authoring.js +2 -1
- package/dist/learning/reward-authoring.js +3 -3
- package/dist/rewards.js +5 -0
- package/dist/types/experiments.d.ts +642 -0
- package/dist/types/experiments.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/learning/admission.d.ts +1 -0
- package/dist/types/learning/admission.d.ts.map +1 -1
- package/dist/types/learning/authoring-service.d.ts +1 -0
- package/dist/types/learning/authoring-service.d.ts.map +1 -1
- package/dist/types/learning/authoring.d.ts +4 -0
- package/dist/types/learning/authoring.d.ts.map +1 -1
- package/dist/types/learning/contracts.d.ts +4 -3
- package/dist/types/learning/contracts.d.ts.map +1 -1
- package/dist/types/learning/iteration-candidate-decision.d.ts +1 -1
- package/dist/types/learning/operations.d.ts +6 -0
- package/dist/types/learning/operations.d.ts.map +1 -1
- package/dist/types/learning/repository.d.ts +5 -3
- 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 +1 -0
- package/dist/types/learning/reward-calibration.d.ts.map +1 -1
- package/dist/types/learning/transport.d.ts +10 -5
- package/dist/types/learning/transport.d.ts.map +1 -1
- package/dist/types/rewards.d.ts +4 -0
- package/dist/types/rewards.d.ts.map +1 -1
- package/package.json +5 -1
- package/schemas/learning/v1/command-request.schema.json +248 -235
- package/schemas/learning/v1/draft.schema.json +4 -0
- package/schemas/learning/v1/reward.schema.json +4 -0
package/CONTRACT.md
CHANGED
|
@@ -162,3 +162,23 @@ Node subprocess for hosts without Node worker APIs. It strips authored TypeScrip
|
|
|
162
162
|
inside its deadline, evaluates the verified source in the same deterministic
|
|
163
163
|
isolate, and exits before success, failure or cancellation settles. Importing
|
|
164
164
|
the metrics entrypoint does not require Node's type-stripping API on the host.
|
|
165
|
+
|
|
166
|
+
## Portable Experiment comparison
|
|
167
|
+
|
|
168
|
+
`verifyExperimentEvidence` verifies the manifest hash, result hash, complete
|
|
169
|
+
case population and exact evaluator bindings. Historical runs without a whole
|
|
170
|
+
execution ceiling report `maximumCostUsd: null`. Dataset-authored graders retain
|
|
171
|
+
their actual version string and implementation hash; reusable Rewards retain
|
|
172
|
+
their numeric release revision. Authored fixture checks use a fixture target.
|
|
173
|
+
|
|
174
|
+
Hosts can pin package, runtime and metric-policy hashes in `execution`. Shared
|
|
175
|
+
comparison requires equal dataset version, population, evaluators and execution
|
|
176
|
+
contract. Harness comparison also requires the same component, evaluation
|
|
177
|
+
definition and environment; candidate source/Harness revisions can differ.
|
|
178
|
+
|
|
179
|
+
`compareExperiments` aligns cases and reports per-feedback eligibility, exclusion
|
|
180
|
+
reasons and paired means/deltas. Only matching completed cases with scored
|
|
181
|
+
numeric or Boolean feedback contribute. Failed, cancelled, absent or ungraded
|
|
182
|
+
cases never become zero-valued measurements. Categorical feedback remains
|
|
183
|
+
inspectable without a numeric delta. This operation reads retained evidence and
|
|
184
|
+
has no execution, grading or billing authority.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ImmutableArtifactRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, assertContentHash, contentHash, } from "@openpond/harness";
|
|
3
|
+
import { ProfileEvaluationRunSourceSchema } from "./profile-evaluations.js";
|
|
4
|
+
import { FeedbackKeySchema } from "./rewards.js";
|
|
5
|
+
const MoneySchema = z.number().finite().nonnegative();
|
|
6
|
+
export const ExperimentTargetSchema = z.discriminatedUnion("kind", [
|
|
7
|
+
z.object({
|
|
8
|
+
kind: z.literal("model"),
|
|
9
|
+
modelId: ReleaseIdSchema,
|
|
10
|
+
configurationHash: ReleaseHashSchema,
|
|
11
|
+
}).strict(),
|
|
12
|
+
z.object({
|
|
13
|
+
kind: z.literal("harness"),
|
|
14
|
+
source: ProfileEvaluationRunSourceSchema,
|
|
15
|
+
}).strict(),
|
|
16
|
+
z.object({ kind: z.literal("fixture"), configurationHash: ReleaseHashSchema }).strict(),
|
|
17
|
+
]);
|
|
18
|
+
export const ExperimentEvaluatorSchema = z.object({
|
|
19
|
+
// A reusable Reward has a numeric revision; a dataset-authored GraderSpec
|
|
20
|
+
// has its exact version string and implementation hash. Neither is invented.
|
|
21
|
+
release: ImmutableReleaseRefSchema.extend({ revision: z.union([z.number().int().positive(), z.string().trim().min(1).max(200)]) }).strict(),
|
|
22
|
+
feedbackKey: FeedbackKeySchema,
|
|
23
|
+
/** Binds field mappings and admitted scorer settings without changing the Reward's release hash. */
|
|
24
|
+
configurationHash: ReleaseHashSchema.optional(),
|
|
25
|
+
output: z.enum(["boolean", "score", "category"]),
|
|
26
|
+
/** Category labels are meaningful only for category output. */
|
|
27
|
+
categories: z.array(z.string().trim().min(1).max(120)).max(50),
|
|
28
|
+
}).strict().superRefine((value, context) => {
|
|
29
|
+
if ((value.output === "category") !== (value.categories.length > 0))
|
|
30
|
+
context.addIssue({ code: "custom", path: ["categories"], message: "Only category feedback declares categories." });
|
|
31
|
+
if (new Set(value.categories).size !== value.categories.length)
|
|
32
|
+
context.addIssue({ code: "custom", path: ["categories"], message: "Feedback categories must be unique." });
|
|
33
|
+
});
|
|
34
|
+
export const ExperimentCaseIdentitySchema = z.object({
|
|
35
|
+
caseId: ReleaseIdSchema,
|
|
36
|
+
seed: z.string().trim().min(1).max(500),
|
|
37
|
+
fixtureId: ReleaseIdSchema.nullable(),
|
|
38
|
+
}).strict();
|
|
39
|
+
export function experimentCaseKey(identity) {
|
|
40
|
+
const value = ExperimentCaseIdentitySchema.parse(identity);
|
|
41
|
+
return JSON.stringify([value.caseId, value.seed, value.fixtureId]);
|
|
42
|
+
}
|
|
43
|
+
export const ExperimentManifestContentSchema = z.object({
|
|
44
|
+
schemaVersion: z.literal("openpond.experimentManifest.v1"),
|
|
45
|
+
id: ReleaseIdSchema,
|
|
46
|
+
name: z.string().trim().min(1).max(500),
|
|
47
|
+
teamId: ReleaseIdSchema,
|
|
48
|
+
operationId: ReleaseIdSchema,
|
|
49
|
+
/** Historical imported runs may have no admitted whole-run ceiling. */
|
|
50
|
+
maximumCostUsd: MoneySchema.nullable(),
|
|
51
|
+
dataset: ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).strict(),
|
|
52
|
+
target: ExperimentTargetSchema,
|
|
53
|
+
execution: z.object({ packageHash: ReleaseHashSchema, runtimeTargetHash: ReleaseHashSchema, metricPolicyHash: ReleaseHashSchema }).strict().optional(),
|
|
54
|
+
lineage: z.object({
|
|
55
|
+
definition: ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).strict().nullable(),
|
|
56
|
+
execution: ImmutableReleaseRefSchema,
|
|
57
|
+
scoringPassId: ReleaseIdSchema.nullable(),
|
|
58
|
+
}).strict().optional(),
|
|
59
|
+
evaluators: z.array(ExperimentEvaluatorSchema).max(100),
|
|
60
|
+
population: z.array(ExperimentCaseIdentitySchema).min(1).max(10_000),
|
|
61
|
+
createdAt: ReleaseTimestampSchema,
|
|
62
|
+
}).strict().superRefine((value, context) => {
|
|
63
|
+
if (value.lineage && value.id !== (value.lineage.scoringPassId ?? value.lineage.execution.id))
|
|
64
|
+
context.addIssue({ code: "custom", path: ["lineage"], message: "Experiment identity must match its execution or independent scoring pass." });
|
|
65
|
+
if (new Set(value.population.map(experimentCaseKey)).size !== value.population.length)
|
|
66
|
+
context.addIssue({ code: "custom", path: ["population"], message: "Experiment cases must be unique." });
|
|
67
|
+
if (new Set(value.evaluators.map((item) => item.feedbackKey)).size !== value.evaluators.length)
|
|
68
|
+
context.addIssue({ code: "custom", path: ["evaluators"], message: "Experiment feedback keys must be unique." });
|
|
69
|
+
});
|
|
70
|
+
export const ExperimentManifestSchema = ExperimentManifestContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
|
|
71
|
+
export const ExperimentFeedbackSchema = z.object({
|
|
72
|
+
feedbackKey: FeedbackKeySchema,
|
|
73
|
+
evaluator: ExperimentEvaluatorSchema.shape.release,
|
|
74
|
+
status: z.enum(["scored", "unavailable", "failed", "pending"]),
|
|
75
|
+
value: z.union([z.boolean(), z.number().finite(), z.string().max(120)]).nullable(),
|
|
76
|
+
passed: z.boolean().nullable().optional(),
|
|
77
|
+
reasoning: z.string().max(20_000).nullable(),
|
|
78
|
+
evidenceRefs: z.array(ImmutableArtifactRefSchema).max(1_000),
|
|
79
|
+
}).strict();
|
|
80
|
+
export const ExperimentUsageSchema = z.object({
|
|
81
|
+
inputTokens: z.number().int().nonnegative().nullable(),
|
|
82
|
+
outputTokens: z.number().int().nonnegative().nullable(),
|
|
83
|
+
totalTokens: z.number().int().nonnegative().nullable(),
|
|
84
|
+
costUsd: MoneySchema.nullable(),
|
|
85
|
+
latencyMs: z.number().finite().nonnegative().nullable(),
|
|
86
|
+
}).strict().superRefine((usage, context) => {
|
|
87
|
+
if (usage.inputTokens !== null && usage.outputTokens !== null && usage.totalTokens !== null
|
|
88
|
+
&& usage.totalTokens < usage.inputTokens + usage.outputTokens)
|
|
89
|
+
context.addIssue({ code: "custom", path: ["totalTokens"], message: "Total tokens cannot be less than input plus output." });
|
|
90
|
+
});
|
|
91
|
+
export const ExperimentCaseResultSchema = z.object({
|
|
92
|
+
identity: ExperimentCaseIdentitySchema,
|
|
93
|
+
status: z.enum(["completed", "failed", "cancelled", "unavailable"]),
|
|
94
|
+
output: z.unknown().nullable(),
|
|
95
|
+
error: z.object({ code: ReleaseIdSchema, message: z.string().max(2_000) }).strict().nullable(),
|
|
96
|
+
feedback: z.array(ExperimentFeedbackSchema).max(100),
|
|
97
|
+
usage: ExperimentUsageSchema,
|
|
98
|
+
traceRef: ImmutableArtifactRefSchema.nullable(),
|
|
99
|
+
startedAt: ReleaseTimestampSchema.nullable(),
|
|
100
|
+
completedAt: ReleaseTimestampSchema.nullable(),
|
|
101
|
+
}).strict();
|
|
102
|
+
export const ExperimentResultContentSchema = z.object({
|
|
103
|
+
schemaVersion: z.literal("openpond.experimentResult.v1"),
|
|
104
|
+
manifest: ImmutableReleaseRefSchema,
|
|
105
|
+
status: z.enum(["completed", "failed", "cancelled"]),
|
|
106
|
+
cases: z.array(ExperimentCaseResultSchema).min(1).max(10_000),
|
|
107
|
+
completedAt: ReleaseTimestampSchema,
|
|
108
|
+
}).strict();
|
|
109
|
+
export const ExperimentResultSchema = ExperimentResultContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
|
|
110
|
+
export function createExperimentManifest(input) {
|
|
111
|
+
const value = ExperimentManifestContentSchema.parse(input);
|
|
112
|
+
return ExperimentManifestSchema.parse({ ...value, contentHash: contentHash(value) });
|
|
113
|
+
}
|
|
114
|
+
export function createExperimentResult(input, manifest) {
|
|
115
|
+
ExperimentManifestSchema.parse(manifest);
|
|
116
|
+
assertContentHash(manifest, "Experiment manifest");
|
|
117
|
+
const value = ExperimentResultContentSchema.parse(input);
|
|
118
|
+
if (value.manifest.id !== manifest.id || value.manifest.contentHash !== manifest.contentHash)
|
|
119
|
+
throw new Error("Experiment result differs from its manifest.");
|
|
120
|
+
const expected = new Set(manifest.population.map(experimentCaseKey));
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const evaluators = new Map(manifest.evaluators.map((item) => [item.feedbackKey, item]));
|
|
123
|
+
for (const result of value.cases) {
|
|
124
|
+
const key = experimentCaseKey(result.identity);
|
|
125
|
+
if (!expected.has(key) || seen.has(key))
|
|
126
|
+
throw new Error("Experiment result has an unexpected or repeated case.");
|
|
127
|
+
seen.add(key);
|
|
128
|
+
const feedbackKeys = new Set();
|
|
129
|
+
for (const feedback of result.feedback) {
|
|
130
|
+
const evaluator = evaluators.get(feedback.feedbackKey);
|
|
131
|
+
if (!evaluator || feedbackKeys.has(feedback.feedbackKey)
|
|
132
|
+
|| feedback.evaluator.id !== evaluator.release.id
|
|
133
|
+
|| feedback.evaluator.contentHash !== evaluator.release.contentHash
|
|
134
|
+
|| feedback.evaluator.revision !== evaluator.release.revision)
|
|
135
|
+
throw new Error("Experiment feedback differs from its pinned evaluator.");
|
|
136
|
+
feedbackKeys.add(feedback.feedbackKey);
|
|
137
|
+
if (feedback.status === "scored") {
|
|
138
|
+
if (feedback.value === null || typeof feedback.value !== { boolean: "boolean", score: "number", category: "string" }[evaluator.output]
|
|
139
|
+
|| (evaluator.output === "category" && !evaluator.categories.includes(feedback.value))
|
|
140
|
+
|| (evaluator.output === "score" && (feedback.value < 0 || feedback.value > 1)))
|
|
141
|
+
throw new Error("Experiment feedback value violates its output contract.");
|
|
142
|
+
}
|
|
143
|
+
else if (feedback.value !== null || feedback.passed !== null && feedback.passed !== undefined)
|
|
144
|
+
throw new Error("Unscored experiment feedback cannot claim a value or pass decision.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (seen.size !== expected.size)
|
|
148
|
+
throw new Error("Experiment result omits admitted cases.");
|
|
149
|
+
return ExperimentResultSchema.parse({ ...value, contentHash: contentHash(value) });
|
|
150
|
+
}
|
|
151
|
+
export function verifyExperimentEvidence(input) {
|
|
152
|
+
const manifest = ExperimentManifestSchema.parse(input.manifest);
|
|
153
|
+
const result = ExperimentResultSchema.parse(input.result);
|
|
154
|
+
const { contentHash: hash, ...content } = result;
|
|
155
|
+
if (contentHash(content) !== hash)
|
|
156
|
+
throw new Error("Experiment result integrity failed.");
|
|
157
|
+
createExperimentResult(content, manifest);
|
|
158
|
+
return { manifest, result };
|
|
159
|
+
}
|
|
160
|
+
/** A comparison reads retained results; it never executes or regrades a case. */
|
|
161
|
+
export function compareExperiments(baseline, candidate) {
|
|
162
|
+
for (const pair of [baseline, candidate]) {
|
|
163
|
+
verifyExperimentEvidence(pair);
|
|
164
|
+
}
|
|
165
|
+
const reasons = [];
|
|
166
|
+
if (baseline.manifest.teamId !== candidate.manifest.teamId)
|
|
167
|
+
reasons.push("different_workspace");
|
|
168
|
+
if (baseline.manifest.dataset.id !== candidate.manifest.dataset.id
|
|
169
|
+
|| baseline.manifest.dataset.contentHash !== candidate.manifest.dataset.contentHash
|
|
170
|
+
|| baseline.manifest.dataset.revision !== candidate.manifest.dataset.revision)
|
|
171
|
+
reasons.push("different_dataset_version");
|
|
172
|
+
if (baseline.manifest.target.kind !== candidate.manifest.target.kind)
|
|
173
|
+
reasons.push("different_target_kind");
|
|
174
|
+
if (contentHash(baseline.manifest.execution ?? null) !== contentHash(candidate.manifest.execution ?? null))
|
|
175
|
+
reasons.push("different_execution_contract");
|
|
176
|
+
if (baseline.manifest.target.kind === "harness" && candidate.manifest.target.kind === "harness") {
|
|
177
|
+
const a = baseline.manifest.target.source;
|
|
178
|
+
const b = candidate.manifest.target.source;
|
|
179
|
+
if (a.profileId !== b.profileId || contentHash(a.target) !== contentHash(b.target))
|
|
180
|
+
reasons.push("different_component");
|
|
181
|
+
if (a.environmentHash !== b.environmentHash)
|
|
182
|
+
reasons.push("different_environment");
|
|
183
|
+
if (a.definitionId !== b.definitionId || a.definitionHash !== b.definitionHash)
|
|
184
|
+
reasons.push("different_evaluation_definition");
|
|
185
|
+
}
|
|
186
|
+
const orderedEvaluators = (manifest) => [...manifest.evaluators].sort((a, b) => a.feedbackKey.localeCompare(b.feedbackKey));
|
|
187
|
+
if (contentHash(orderedEvaluators(baseline.manifest)) !== contentHash(orderedEvaluators(candidate.manifest)))
|
|
188
|
+
reasons.push("different_evaluators");
|
|
189
|
+
const left = new Map(baseline.result.cases.map((item) => [experimentCaseKey(item.identity), item]));
|
|
190
|
+
const right = new Map(candidate.result.cases.map((item) => [experimentCaseKey(item.identity), item]));
|
|
191
|
+
const keys = [...new Set([...left.keys(), ...right.keys()])].sort();
|
|
192
|
+
if (left.size !== right.size || keys.some((key) => !left.has(key) || !right.has(key)))
|
|
193
|
+
reasons.push("different_population");
|
|
194
|
+
const comparable = reasons.length === 0;
|
|
195
|
+
const cases = keys.map((key) => {
|
|
196
|
+
const baselineCase = left.get(key) ?? null;
|
|
197
|
+
const candidateCase = right.get(key) ?? null;
|
|
198
|
+
const baselineFeedback = new Map(baselineCase?.feedback.map(item => [item.feedbackKey, item]) ?? []);
|
|
199
|
+
const candidateFeedback = new Map(candidateCase?.feedback.map(item => [item.feedbackKey, item]) ?? []);
|
|
200
|
+
return {
|
|
201
|
+
identity: (baselineCase ?? candidateCase).identity,
|
|
202
|
+
baseline: baselineCase,
|
|
203
|
+
candidate: candidateCase,
|
|
204
|
+
feedback: baseline.manifest.evaluators.map(evaluator => {
|
|
205
|
+
const a = baselineFeedback.get(evaluator.feedbackKey);
|
|
206
|
+
const b = candidateFeedback.get(evaluator.feedbackKey);
|
|
207
|
+
const reason = !comparable ? "incompatible_experiments"
|
|
208
|
+
: !baselineCase || !candidateCase ? "missing_case"
|
|
209
|
+
: baselineCase.status !== "completed" || candidateCase.status !== "completed" || baselineCase.error || candidateCase.error ? "case_not_completed"
|
|
210
|
+
: !a || !b || a.status !== "scored" || b.status !== "scored" ? "feedback_unavailable"
|
|
211
|
+
: evaluator.output === "category" ? "categorical_feedback" : null;
|
|
212
|
+
const numeric = (item) => typeof item?.value === "boolean" ? Number(item.value)
|
|
213
|
+
: typeof item?.value === "number" ? item.value : null;
|
|
214
|
+
const av = reason ? null : numeric(a);
|
|
215
|
+
const bv = reason ? null : numeric(b);
|
|
216
|
+
const pass = (item) => reason ? null : typeof item?.value === "boolean" ? item.value : item?.passed ?? null;
|
|
217
|
+
return { feedbackKey: evaluator.feedbackKey, eligible: reason === null, reason,
|
|
218
|
+
baseline: av, candidate: bv, delta: av === null || bv === null ? null : bv - av,
|
|
219
|
+
baselinePassed: pass(a), candidatePassed: pass(b) };
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
const metrics = baseline.manifest.evaluators.map((evaluator, index) => {
|
|
224
|
+
const eligible = cases.map(item => item.feedback[index]).filter(value => value.eligible);
|
|
225
|
+
const mean = (field) => eligible.length
|
|
226
|
+
? eligible.reduce((sum, value) => sum + value[field], 0) / eligible.length : null;
|
|
227
|
+
const a = mean("baseline");
|
|
228
|
+
const b = mean("candidate");
|
|
229
|
+
const passRate = (field) => eligible.length && eligible.every(value => value[field] !== null)
|
|
230
|
+
? eligible.reduce((sum, value) => sum + Number(value[field]), 0) / eligible.length : null;
|
|
231
|
+
const ap = passRate("baselinePassed");
|
|
232
|
+
const bp = passRate("candidatePassed");
|
|
233
|
+
return { feedbackKey: evaluator.feedbackKey, eligibleCount: eligible.length, excludedCount: cases.length - eligible.length,
|
|
234
|
+
baseline: a, candidate: b, delta: a === null || b === null ? null : b - a,
|
|
235
|
+
baselinePassRate: ap, candidatePassRate: bp, passRateDelta: ap === null || bp === null ? null : bp - ap };
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
comparable,
|
|
239
|
+
reasons,
|
|
240
|
+
cases,
|
|
241
|
+
metrics,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./artifact-verification.js";
|
|
|
4
4
|
export * from "./evidence/index.js";
|
|
5
5
|
export * from "./execution-contracts.js";
|
|
6
6
|
export * from "./execution-receipts.js";
|
|
7
|
+
export * from "./experiments.js";
|
|
7
8
|
export * from "./graders.js";
|
|
8
9
|
export * from "./harness.js";
|
|
9
10
|
export * from "./learned-preference.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
|
|
3
|
-
import { RewardBindingSourceSchema, RewardReleaseRefSchema } from "../rewards.js";
|
|
3
|
+
import { FeedbackKeySchema, RewardBindingSourceSchema, RewardReleaseRefSchema } from "../rewards.js";
|
|
4
4
|
import { LearningRevisionRefSchema } from "./contracts.js";
|
|
5
5
|
/** Strings intentionally preserve incomplete JSON/code until explicit publication. */
|
|
6
6
|
export const RewardFixtureAuthoringFieldsSchema = z.object({
|
|
@@ -14,6 +14,7 @@ export const RewardFixtureAuthoringFieldsSchema = z.object({
|
|
|
14
14
|
}).strict();
|
|
15
15
|
export const RewardAuthoringFieldsSchema = z.object({
|
|
16
16
|
name: z.string().max(500), description: z.string().max(10_000),
|
|
17
|
+
feedbackKey: FeedbackKeySchema.optional(),
|
|
17
18
|
kind: z.enum(["custom_verifier", "state", "content", "schema", "artifact", "runtime_event", "model_judge", "learned_model", "human"]),
|
|
18
19
|
fields: z.string(), outputField: z.string(), expectedField: z.string(), expectedValue: z.string(),
|
|
19
20
|
schema: z.string(), reference: z.string(), events: z.string(), code: z.string(), exportName: z.string(), timeout: z.string(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RewardReleaseSchema, createRewardRelease } from "../rewards.js";
|
|
1
|
+
import { RewardReleaseSchema, createRewardRelease, feedbackKeyForReward } 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";
|
|
@@ -64,7 +64,7 @@ export function compileRewardAuthoring(input) {
|
|
|
64
64
|
const assets = [...(asset ? [asset] : []), ...(fixtureAsset ? [fixtureAsset] : [])];
|
|
65
65
|
const reward = createRewardRelease({
|
|
66
66
|
schemaVersion: "openpond.rewardRelease.v1", id: input.id, revision: (base?.revision ?? 0) + 1,
|
|
67
|
-
name: fields.name, description: fields.description, implementation,
|
|
67
|
+
name: fields.name, description: fields.description, feedbackKey: fields.feedbackKey ?? feedbackKeyForReward({ id: input.id }), implementation,
|
|
68
68
|
rawScore: fields.kind === "learned_model" ? { minimum: authoringNumber(fields.minimum, "Minimum score"), maximum: authoringNumber(fields.maximum, "Maximum score") } : { minimum: 0, maximum: 1 },
|
|
69
69
|
assets: assets.map(value => value.asset), ...(fixtureAsset ? { fixtureSetRef: fixtureAsset.asset } : {}),
|
|
70
70
|
...(calibrationCheckRef ? { calibrationCheckRef } : {}),
|
|
@@ -84,7 +84,7 @@ export function rewardAuthoringFields(reward, sourceAsset, fixtureAsset = null)
|
|
|
84
84
|
const fixtures = reward?.fixtureSetRef && fixtureAsset ? RewardFixtureAuthoringFieldsSchema.array().max(50).parse(JSON.parse(verifyLearningTextAsset(fixtureAsset, reward.fixtureSetRef))) : [];
|
|
85
85
|
const config = implementation && "config" in implementation ? implementation.config : {};
|
|
86
86
|
return {
|
|
87
|
-
name: reward?.name ?? "", description: reward?.description ?? "", kind: implementation?.kind ?? "state",
|
|
87
|
+
name: reward?.name ?? "", description: reward?.description ?? "", ...(reward?.feedbackKey ? { feedbackKey: reward.feedbackKey } : {}), kind: implementation?.kind ?? "state",
|
|
88
88
|
fields: strings(config.fields).join(", ") || "answer", outputField: text(config.outputField) || "text", expectedField: text(config.expectedField) || "text", expectedValue: text(config.expectedValue),
|
|
89
89
|
schema: JSON.stringify(config.jsonSchema ?? { type: "object", properties: { answer: { type: "string" } }, required: ["answer"] }, null, 2),
|
|
90
90
|
reference: text(config.refIncludes), events: strings(config.requiredEvents).join(", "),
|
package/dist/rewards.js
CHANGED
|
@@ -5,6 +5,10 @@ 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 };
|
|
8
|
+
export const FeedbackKeySchema = z.string().regex(/^[a-z][a-z0-9_]{0,79}$/);
|
|
9
|
+
export function feedbackKeyForReward(reward) {
|
|
10
|
+
return reward.feedbackKey ?? `reward_${contentHash(reward.id).slice(0, 24)}`;
|
|
11
|
+
}
|
|
8
12
|
export const RewardImplementationSchema = z.union([
|
|
9
13
|
DeterministicGraderSpecSchema.omit(bindingFields),
|
|
10
14
|
ModelJudgeGraderSpecSchema.omit(bindingFields),
|
|
@@ -18,6 +22,7 @@ export const RewardReleaseContentSchema = z.object({
|
|
|
18
22
|
revision: z.number().int().positive(),
|
|
19
23
|
name: z.string().trim().min(1).max(500),
|
|
20
24
|
description: z.string().max(10_000),
|
|
25
|
+
feedbackKey: FeedbackKeySchema.optional(),
|
|
21
26
|
implementation: RewardImplementationSchema,
|
|
22
27
|
rawScore: z.object({ minimum: z.number().finite(), maximum: z.number().finite() }).strict(),
|
|
23
28
|
assets: z.array(ImmutableAssetRefSchema).max(1_000),
|