@openpond/evals 0.7.5 → 0.8.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/JAVASCRIPT_ENVIRONMENTS.md +47 -0
- package/LEARNING.md +30 -0
- package/dist/environment-execution-services-contract.js +27 -0
- package/dist/environment-execution-services.js +100 -0
- package/dist/javascript-environment-contract.js +13 -0
- package/dist/javascript-environment-node.js +4 -2
- package/dist/javascript-environment.js +4 -2
- package/dist/javascript-isolate-process-entry.js +2 -2
- package/dist/javascript-isolate-process-source.js +1 -1
- package/dist/javascript-isolate-process.js +6 -4
- package/dist/javascript-isolate.js +39 -30
- package/dist/javascript-verifier-worker-source.js +1 -1
- package/dist/learning/authoring-service.js +1 -1
- package/dist/learning/authoring.js +9 -0
- package/dist/learning/index.js +4 -0
- package/dist/learning/operations.js +3 -1
- package/dist/learning/repository.js +2 -0
- package/dist/learning/reward-authoring.js +120 -0
- package/dist/learning/reward-check-service.js +31 -0
- package/dist/learning/reward-check-worker.js +114 -0
- package/dist/learning/reward-checks.js +81 -0
- package/dist/learning/reward-fixture-executor.js +29 -0
- package/dist/learning/service.js +7 -0
- package/dist/rewards.js +3 -0
- package/dist/sql-execution-contract.js +52 -0
- package/dist/sql-execution-engine.js +135 -0
- package/dist/sql-execution-node.js +58 -0
- package/dist/sql-execution-process-entry.js +3 -0
- package/dist/sql-execution-process-source.js +1 -0
- package/dist/sql-wasm-binary.js +1 -0
- package/dist/types/environment-execution-services-contract.d.ts +74 -0
- package/dist/types/environment-execution-services-contract.d.ts.map +1 -0
- package/dist/types/environment-execution-services.d.ts +17 -0
- package/dist/types/environment-execution-services.d.ts.map +1 -0
- package/dist/types/evidence/conformance.d.ts +4 -4
- package/dist/types/evidence/contracts.d.ts +4 -4
- package/dist/types/javascript-environment-contract.d.ts +121 -0
- package/dist/types/javascript-environment-contract.d.ts.map +1 -1
- package/dist/types/javascript-environment-node.d.ts.map +1 -1
- package/dist/types/javascript-environment.d.ts +36 -0
- package/dist/types/javascript-environment.d.ts.map +1 -1
- package/dist/types/javascript-isolate-process.d.ts.map +1 -1
- package/dist/types/javascript-isolate.d.ts +4 -1
- package/dist/types/javascript-isolate.d.ts.map +1 -1
- package/dist/types/learning/admission.d.ts +12 -0
- package/dist/types/learning/admission.d.ts.map +1 -1
- package/dist/types/learning/authoring-service.d.ts +172 -0
- package/dist/types/learning/authoring-service.d.ts.map +1 -1
- package/dist/types/learning/authoring.d.ts +121 -0
- package/dist/types/learning/authoring.d.ts.map +1 -1
- package/dist/types/learning/contracts.d.ts +23 -11
- package/dist/types/learning/contracts.d.ts.map +1 -1
- package/dist/types/learning/index.d.ts +4 -0
- package/dist/types/learning/index.d.ts.map +1 -1
- package/dist/types/learning/operations.d.ts +134 -6
- package/dist/types/learning/operations.d.ts.map +1 -1
- package/dist/types/learning/repository.d.ts +114 -5
- package/dist/types/learning/repository.d.ts.map +1 -1
- package/dist/types/learning/reward-authoring.d.ts +18 -0
- package/dist/types/learning/reward-authoring.d.ts.map +1 -0
- package/dist/types/learning/reward-check-service.d.ts +17 -0
- package/dist/types/learning/reward-check-service.d.ts.map +1 -0
- package/dist/types/learning/reward-check-worker.d.ts +27 -0
- package/dist/types/learning/reward-check-worker.d.ts.map +1 -0
- package/dist/types/learning/reward-checks.d.ts +177 -0
- package/dist/types/learning/reward-checks.d.ts.map +1 -0
- package/dist/types/learning/reward-fixture-executor.d.ts +9 -0
- package/dist/types/learning/reward-fixture-executor.d.ts.map +1 -0
- package/dist/types/learning/service.d.ts.map +1 -1
- package/dist/types/learning/transport.d.ts +200 -28
- package/dist/types/learning/transport.d.ts.map +1 -1
- package/dist/types/rewards.d.ts +24 -0
- package/dist/types/rewards.d.ts.map +1 -1
- package/dist/types/sql-execution-contract.d.ts +72 -0
- package/dist/types/sql-execution-contract.d.ts.map +1 -0
- package/dist/types/sql-execution-engine.d.ts +4 -0
- package/dist/types/sql-execution-engine.d.ts.map +1 -0
- package/dist/types/sql-execution-node.d.ts +9 -0
- package/dist/types/sql-execution-node.d.ts.map +1 -0
- package/dist/types/sql-execution-process-entry.d.ts +2 -0
- package/dist/types/sql-execution-process-entry.d.ts.map +1 -0
- package/dist/types/sql-execution-process-source.d.ts +1 -0
- package/dist/types/sql-wasm-binary.d.ts +1 -0
- package/package.json +10 -1
- package/schemas/learning/v1/command-request.schema.json +412 -248
- package/schemas/learning/v1/draft.schema.json +96 -0
- package/schemas/learning/v1/read-request.schema.json +1 -0
- package/schemas/learning/v1/reward.schema.json +3 -0
- package/schemas/learning/v1/reward_check.schema.json +343 -0
|
@@ -38,7 +38,7 @@ export async function finalizeAuthoringDraft(tx, input, published, now) {
|
|
|
38
38
|
await tx.put("draft", completed, draft.revision, { parentId: draft.targetKind, status: completed.status });
|
|
39
39
|
return { kind: "draft", id: completed.id, revision: completed.revision };
|
|
40
40
|
}
|
|
41
|
-
async function currentDraft(tx, ref) {
|
|
41
|
+
export async function currentDraft(tx, ref) {
|
|
42
42
|
const draft = await requireLearningRelease(tx, "draft", ref);
|
|
43
43
|
const current = await tx.get("draft", ref.id);
|
|
44
44
|
if (!current || !sameLearningRef(learningRef(current), ref))
|
|
@@ -3,6 +3,14 @@ import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@ope
|
|
|
3
3
|
import { RewardBindingSourceSchema, RewardReleaseRefSchema } from "../rewards.js";
|
|
4
4
|
import { LearningRevisionRefSchema } from "./contracts.js";
|
|
5
5
|
/** Strings intentionally preserve incomplete JSON/code until explicit publication. */
|
|
6
|
+
export const RewardFixtureAuthoringFieldsSchema = z.object({
|
|
7
|
+
id: ReleaseIdSchema, name: z.string().max(500),
|
|
8
|
+
input: z.string(), output: z.string(), expectedOutput: z.string(), evaluatorContext: z.string(),
|
|
9
|
+
artifactRefs: z.array(z.string().max(500)).max(100), runtimeEventRefs: z.array(z.string().max(500)).max(100),
|
|
10
|
+
infrastructureError: z.string().max(20_000),
|
|
11
|
+
expectedStatus: z.enum(["scored", "pending", "unavailable", "failed"]),
|
|
12
|
+
minimumScore: z.string(), maximumScore: z.string(), expectedPassed: z.enum(["true", "false", "any"]),
|
|
13
|
+
}).strict();
|
|
6
14
|
export const RewardAuthoringFieldsSchema = z.object({
|
|
7
15
|
name: z.string().max(500), description: z.string().max(10_000),
|
|
8
16
|
kind: z.enum(["custom_verifier", "state", "content", "schema", "artifact", "runtime_event", "model_judge", "learned_model", "human"]),
|
|
@@ -10,6 +18,7 @@ export const RewardAuthoringFieldsSchema = z.object({
|
|
|
10
18
|
schema: z.string(), reference: z.string(), events: z.string(), code: z.string(), exportName: z.string(), timeout: z.string(),
|
|
11
19
|
rubric: z.string(), providerId: z.string(), modelId: z.string(), modelRevision: z.string(), temperature: z.string(),
|
|
12
20
|
reviewerRole: z.string(), learnedId: z.string(), learnedHash: z.string(), inputContract: z.string(), minimum: z.string(), maximum: z.string(),
|
|
21
|
+
fixtures: z.array(RewardFixtureAuthoringFieldsSchema).max(50).optional(),
|
|
13
22
|
}).strict();
|
|
14
23
|
export const TaskFormatAuthoringFieldsSchema = z.object({
|
|
15
24
|
name: z.string().max(500), description: z.string().max(10_000), instructions: z.string().max(20_000),
|
package/dist/learning/index.js
CHANGED
|
@@ -8,3 +8,7 @@ export * from "./grade-worker.js";
|
|
|
8
8
|
export * from "./transport.js";
|
|
9
9
|
export * from "./errors.js";
|
|
10
10
|
export * from "./authoring.js";
|
|
11
|
+
export * from "./reward-authoring.js";
|
|
12
|
+
export * from "./reward-checks.js";
|
|
13
|
+
export * from "./reward-check-worker.js";
|
|
14
|
+
export * from "./reward-fixture-executor.js";
|
|
@@ -34,4 +34,6 @@ export const SealTaskBatchCommandSchema = command.extend({ action: z.literal("se
|
|
|
34
34
|
export const CancelTaskGradeCommandSchema = command.extend({ action: z.literal("cancel_grade"), gradeId: ReleaseIdSchema, expectedRevision: z.number().int().positive() }).strict();
|
|
35
35
|
export const SaveAuthoringDraftCommandSchema = command.extend({ action: z.literal("save_draft"), expectedRevision: z.number().int().nonnegative(), draft: AuthoringDraftInputSchema }).strict();
|
|
36
36
|
export const ArchiveAuthoringDraftCommandSchema = command.extend({ action: z.literal("archive_draft"), draft: LearningRevisionRefSchema }).strict();
|
|
37
|
-
export const
|
|
37
|
+
export const QueueRewardCheckCommandSchema = command.extend({ action: z.literal("queue_reward_check"), draft: LearningRevisionRefSchema, timeoutMs: z.number().int().min(100).max(300_000).default(30_000), maximumSpendUsd: z.number().nonnegative().max(1_000).default(0) }).strict();
|
|
38
|
+
export const CancelRewardCheckCommandSchema = command.extend({ action: z.literal("cancel_reward_check"), checkId: ReleaseIdSchema, expectedRevision: z.number().int().positive() }).strict();
|
|
39
|
+
export const LearningCommandSchema = z.union([QueueRewardCheckCommandSchema, CancelRewardCheckCommandSchema, SaveAuthoringDraftCommandSchema, ArchiveAuthoringDraftCommandSchema, PublishLearningResourceCommandSchema, PublishLearningResourcesCommandSchema, SubmitTaskExampleCommandSchema, SubmitTaskFeedbackCommandSchema, ApplyTaskCorrectionCommandSchema, ResolveTaskFeedbackCommandSchema, QueueTaskGradeCommandSchema, ReviewTaskEvidenceCommandSchema, SealTaskBatchCommandSchema, CancelTaskGradeCommandSchema]);
|
|
@@ -6,8 +6,10 @@ import { TasksetReleaseSchema } from "../tasksets.js";
|
|
|
6
6
|
import { LearningDomainError } from "./errors.js";
|
|
7
7
|
import { AuthoringDraftSchema } from "./authoring.js";
|
|
8
8
|
import { LearningTextAssetSchema } from "./assets.js";
|
|
9
|
+
import { RewardCheckRunSchema } from "./reward-checks.js";
|
|
9
10
|
export const learningResourceSchemas = {
|
|
10
11
|
draft: AuthoringDraftSchema,
|
|
12
|
+
reward_check: RewardCheckRunSchema,
|
|
11
13
|
asset: LearningTextAssetSchema,
|
|
12
14
|
definition: TaskDefinitionSchema, reward: RewardReleaseSchema, binding: RewardBindingSchema,
|
|
13
15
|
source: LearningSourceSchema, evidence: TaskEvidenceSchema, feedback: TaskFeedbackSchema,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { RewardReleaseSchema, createRewardRelease } from "../rewards.js";
|
|
2
|
+
import { assertBoundedTaskJson } from "../task-schema.js";
|
|
3
|
+
import { RewardAuthoringFieldsSchema, RewardFixtureAuthoringFieldsSchema } from "./authoring.js";
|
|
4
|
+
import { createLearningTextAsset, verifyLearningTextAsset } from "./assets.js";
|
|
5
|
+
import { LearningJsonObjectSchema } from "./contracts.js";
|
|
6
|
+
import { LearningDomainError } from "./errors.js";
|
|
7
|
+
export const DEFAULT_REWARD_VERIFIER_SOURCE = `export function verify({ output, expectedOutput }) {
|
|
8
|
+
if (!expectedOutput || !Object.hasOwn(expectedOutput, "answer")) {
|
|
9
|
+
throw new Error("An expected answer is required.");
|
|
10
|
+
}
|
|
11
|
+
const passed = output.answer === expectedOutput.answer;
|
|
12
|
+
return { score: passed ? 1 : 0, passed, feedback: passed ? "Answer matched" : "Answer differed" };
|
|
13
|
+
}`;
|
|
14
|
+
/** Both editors and execution owners compile the same draft bytes. No publication occurs here. */
|
|
15
|
+
export function compileRewardAuthoring(input) {
|
|
16
|
+
assertBoundedTaskJson(input, 262_144);
|
|
17
|
+
const fields = RewardAuthoringFieldsSchema.parse(input.fields);
|
|
18
|
+
const base = input.base ? RewardReleaseSchema.parse(input.base) : null;
|
|
19
|
+
if (base && base.id !== input.id)
|
|
20
|
+
throw new LearningDomainError("reward_authoring_base_mismatch", 422);
|
|
21
|
+
const fixtureAsset = fields.fixtures?.length ? createLearningTextAsset({ text: JSON.stringify(fields.fixtures), path: "reward-fixtures.json", mediaType: "application/json", visibility: "verifier" }) : null;
|
|
22
|
+
let asset = null;
|
|
23
|
+
let implementation;
|
|
24
|
+
if (fields.kind === "custom_verifier") {
|
|
25
|
+
asset = createLearningTextAsset({ text: fields.code, path: "verifier.mjs", mediaType: "application/javascript", visibility: "verifier" });
|
|
26
|
+
implementation = { kind: fields.kind, verifierRef: asset.asset, exportName: fields.exportName, timeoutMs: authoringNumber(fields.timeout, "Time limit"), networkPolicy: "none" };
|
|
27
|
+
}
|
|
28
|
+
else if (fields.kind === "model_judge" || fields.kind === "human") {
|
|
29
|
+
if (!fields.rubric.trim())
|
|
30
|
+
throw new LearningDomainError("reward_rubric_required", 422, "Write the rubric before checking or publishing this Reward.");
|
|
31
|
+
asset = createLearningTextAsset({ text: fields.rubric, path: "rubric.md", mediaType: "text/markdown", visibility: "verifier" });
|
|
32
|
+
if (fields.kind === "human")
|
|
33
|
+
implementation = { kind: fields.kind, rubricRef: asset.asset, reviewerRole: fields.reviewerRole };
|
|
34
|
+
else {
|
|
35
|
+
const model = { providerId: fields.providerId, modelId: fields.modelId, revision: fields.modelRevision.trim() || null };
|
|
36
|
+
const temperature = authoringNumber(fields.temperature, "Temperature");
|
|
37
|
+
const previous = base?.implementation;
|
|
38
|
+
const unchanged = previous?.kind === "model_judge" && previous.rubricRef.contentHash === asset.asset.contentHash
|
|
39
|
+
&& previous.model?.providerId === model.providerId && previous.model?.modelId === model.modelId
|
|
40
|
+
&& previous.model?.revision === model.revision && (previous.temperature ?? 0) === temperature
|
|
41
|
+
&& base?.fixtureSetRef?.contentHash === fixtureAsset?.asset.contentHash;
|
|
42
|
+
implementation = { kind: fields.kind, rubricRef: asset.asset, model, temperature, calibrationStatus: unchanged ? previous.calibrationStatus : "pending" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (fields.kind === "learned_model") {
|
|
46
|
+
parseRewardAuthoringObject(fields.inputContract, "Model input contract");
|
|
47
|
+
asset = createLearningTextAsset({ text: fields.inputContract, path: "input-contract.json", mediaType: "application/json", visibility: "verifier" });
|
|
48
|
+
implementation = { kind: fields.kind, modelVersion: { id: fields.learnedId, contentHash: fields.learnedHash }, inputContract: asset.asset };
|
|
49
|
+
}
|
|
50
|
+
else if (fields.kind === "state")
|
|
51
|
+
implementation = { kind: fields.kind, config: { fields: split(fields.fields) } };
|
|
52
|
+
else if (fields.kind === "content")
|
|
53
|
+
implementation = { kind: fields.kind, config: { outputField: fields.outputField, expectedField: fields.expectedField, ...(fields.expectedValue ? { expectedValue: fields.expectedValue } : {}) } };
|
|
54
|
+
else if (fields.kind === "schema")
|
|
55
|
+
implementation = { kind: fields.kind, config: { jsonSchema: parseRewardAuthoringObject(fields.schema, "Output schema") } };
|
|
56
|
+
else if (fields.kind === "artifact")
|
|
57
|
+
implementation = { kind: fields.kind, config: { refIncludes: fields.reference } };
|
|
58
|
+
else
|
|
59
|
+
implementation = { kind: fields.kind, config: { requiredEvents: split(fields.events) } };
|
|
60
|
+
const assets = [...(asset ? [asset] : []), ...(fixtureAsset ? [fixtureAsset] : [])];
|
|
61
|
+
const reward = createRewardRelease({
|
|
62
|
+
schemaVersion: "openpond.rewardRelease.v1", id: input.id, revision: (base?.revision ?? 0) + 1,
|
|
63
|
+
name: fields.name, description: fields.description, implementation,
|
|
64
|
+
rawScore: fields.kind === "learned_model" ? { minimum: authoringNumber(fields.minimum, "Minimum score"), maximum: authoringNumber(fields.maximum, "Maximum score") } : { minimum: 0, maximum: 1 },
|
|
65
|
+
assets: assets.map(value => value.asset), ...(fixtureAsset ? { fixtureSetRef: fixtureAsset.asset } : {}),
|
|
66
|
+
});
|
|
67
|
+
return { reward, assets };
|
|
68
|
+
}
|
|
69
|
+
export function rewardAuthoringFields(reward, sourceAsset, fixtureAsset = null) {
|
|
70
|
+
const implementation = reward?.implementation;
|
|
71
|
+
const ref = implementation && "verifierRef" in implementation ? implementation.verifierRef
|
|
72
|
+
: implementation && "rubricRef" in implementation ? implementation.rubricRef
|
|
73
|
+
: implementation && "inputContract" in implementation ? implementation.inputContract : null;
|
|
74
|
+
if (ref && !sourceAsset)
|
|
75
|
+
throw new LearningDomainError("reward_source_required", 422);
|
|
76
|
+
const source = ref && sourceAsset ? verifyLearningTextAsset(sourceAsset, ref) : "";
|
|
77
|
+
if (reward?.fixtureSetRef && !fixtureAsset)
|
|
78
|
+
throw new LearningDomainError("reward_fixtures_required", 422);
|
|
79
|
+
const fixtures = reward?.fixtureSetRef && fixtureAsset ? RewardFixtureAuthoringFieldsSchema.array().max(50).parse(JSON.parse(verifyLearningTextAsset(fixtureAsset, reward.fixtureSetRef))) : [];
|
|
80
|
+
const config = implementation && "config" in implementation ? implementation.config : {};
|
|
81
|
+
return {
|
|
82
|
+
name: reward?.name ?? "", description: reward?.description ?? "", kind: implementation?.kind ?? "state",
|
|
83
|
+
fields: strings(config.fields).join(", ") || "answer", outputField: text(config.outputField) || "text", expectedField: text(config.expectedField) || "text", expectedValue: text(config.expectedValue),
|
|
84
|
+
schema: JSON.stringify(config.jsonSchema ?? { type: "object", properties: { answer: { type: "string" } }, required: ["answer"] }, null, 2),
|
|
85
|
+
reference: text(config.refIncludes), events: strings(config.requiredEvents).join(", "),
|
|
86
|
+
code: implementation?.kind === "custom_verifier" ? source : DEFAULT_REWARD_VERIFIER_SOURCE,
|
|
87
|
+
exportName: implementation?.kind === "custom_verifier" ? implementation.exportName ?? "verify" : "verify",
|
|
88
|
+
timeout: String(implementation?.kind === "custom_verifier" ? implementation.timeoutMs : 5_000),
|
|
89
|
+
rubric: implementation?.kind === "model_judge" || implementation?.kind === "human" ? source : "",
|
|
90
|
+
providerId: implementation?.kind === "model_judge" ? implementation.model?.providerId ?? "" : "",
|
|
91
|
+
modelId: implementation?.kind === "model_judge" ? implementation.model?.modelId ?? "" : "",
|
|
92
|
+
modelRevision: implementation?.kind === "model_judge" ? implementation.model?.revision ?? "" : "",
|
|
93
|
+
temperature: String(implementation?.kind === "model_judge" ? implementation.temperature ?? 0 : 0),
|
|
94
|
+
reviewerRole: implementation?.kind === "human" ? implementation.reviewerRole : "Subject matter reviewer",
|
|
95
|
+
learnedId: implementation?.kind === "learned_model" ? implementation.modelVersion.id : "",
|
|
96
|
+
learnedHash: implementation?.kind === "learned_model" ? implementation.modelVersion.contentHash : "",
|
|
97
|
+
inputContract: implementation?.kind === "learned_model" ? source : "{}",
|
|
98
|
+
minimum: String(reward?.rawScore.minimum ?? 0), maximum: String(reward?.rawScore.maximum ?? 1),
|
|
99
|
+
...(fixtures.length ? { fixtures } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function parseRewardAuthoringObject(value, label) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(value);
|
|
105
|
+
assertBoundedTaskJson(parsed, 131_072);
|
|
106
|
+
return LearningJsonObjectSchema.parse(parsed);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new LearningDomainError("reward_authoring_json_invalid", 422, `${label} must be a JSON object.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function authoringNumber(value, label) {
|
|
113
|
+
const number = Number(value);
|
|
114
|
+
if (!value.trim() || !Number.isFinite(number))
|
|
115
|
+
throw new LearningDomainError("reward_authoring_number_invalid", 422, `${label} must be a finite number.`);
|
|
116
|
+
return number;
|
|
117
|
+
}
|
|
118
|
+
function text(value) { return typeof value === "string" ? value : ""; }
|
|
119
|
+
function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; }
|
|
120
|
+
function split(value) { return value.split(",").map(part => part.trim()).filter(Boolean); }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LearningDomainError } from "./errors.js";
|
|
2
|
+
import { currentDraft } from "./authoring-service.js";
|
|
3
|
+
import { learningRef } from "./contracts.js";
|
|
4
|
+
import { compileRewardCheck, RewardCheckRunSchema } from "./reward-checks.js";
|
|
5
|
+
import { LearningConflictError, requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
6
|
+
export async function queueRewardCheck(tx, input, operationId, now) {
|
|
7
|
+
const draft = await currentDraft(tx, input.draft);
|
|
8
|
+
if (draft.targetKind !== "reward")
|
|
9
|
+
throw new LearningDomainError("reward_check_draft_kind_invalid", 422);
|
|
10
|
+
const base = draft.baseRelease ? await requireLearningRelease(tx, "reward", draft.baseRelease) : null;
|
|
11
|
+
const compiled = compileRewardCheck(draft, base);
|
|
12
|
+
const check = RewardCheckRunSchema.parse({
|
|
13
|
+
schemaVersion: "openpond.rewardCheckRun.v1", id: `reward-check-${operationId}`, revision: 1,
|
|
14
|
+
draft: learningRef(draft), reward: learningRef(compiled.reward), snapshotHash: compiled.snapshotHash,
|
|
15
|
+
fixtureRefs: compiled.fixtureRefs, status: "queued", runtime: null, results: [], matchesExpectations: null,
|
|
16
|
+
timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd,
|
|
17
|
+
leaseOwner: null, leaseExpiresAt: null, attemptCount: 0, failure: null, createdAt: now, updatedAt: now,
|
|
18
|
+
});
|
|
19
|
+
await tx.put("reward_check", check, 0, { parentId: draft.targetId, status: check.status });
|
|
20
|
+
return { kind: "reward_check", id: check.id, revision: check.revision };
|
|
21
|
+
}
|
|
22
|
+
export async function cancelRewardCheck(tx, input, now) {
|
|
23
|
+
const check = await requireLearningResource(tx, "reward_check", input.checkId);
|
|
24
|
+
if (check.revision !== input.expectedRevision)
|
|
25
|
+
throw new LearningConflictError("reward_check", check.id, input.expectedRevision, check.revision);
|
|
26
|
+
if (["completed", "failed", "cancelled"].includes(check.status))
|
|
27
|
+
return { kind: "reward_check", id: check.id, revision: check.revision };
|
|
28
|
+
const updated = RewardCheckRunSchema.parse({ ...check, revision: check.revision + 1, status: check.status === "queued" ? "cancelled" : "cancelling", updatedAt: now });
|
|
29
|
+
await tx.put("reward_check", updated, check.revision, { parentId: check.reward.id, status: updated.status });
|
|
30
|
+
return { kind: "reward_check", id: updated.id, revision: updated.revision };
|
|
31
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { contentHash } from "@openpond/harness";
|
|
2
|
+
import { BoundRewardResultSchema } from "../rewards.js";
|
|
3
|
+
import { sameLearningRef, learningRef } from "./contracts.js";
|
|
4
|
+
import { compileRewardCheck, matchRewardFixture, RewardCheckRunSchema, RewardCheckRuntimeSchema } from "./reward-checks.js";
|
|
5
|
+
import { requireLearningRelease, requireLearningResource } from "./repository.js";
|
|
6
|
+
export function createRewardCheckWorker(repository, executor, options) {
|
|
7
|
+
const runtime = RewardCheckRuntimeSchema.parse(executor.runtime);
|
|
8
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
9
|
+
const active = new Map();
|
|
10
|
+
const key = (scope, id) => JSON.stringify([scope, id]);
|
|
11
|
+
const terminal = (run) => ["completed", "failed", "cancelled"].includes(run.status);
|
|
12
|
+
async function run(scope, checkId) {
|
|
13
|
+
const claim = await repository.transaction(scope, async (tx) => {
|
|
14
|
+
const current = await requireLearningResource(tx, "reward_check", checkId);
|
|
15
|
+
if (terminal(current) || (current.leaseExpiresAt && Date.parse(current.leaseExpiresAt) > Date.parse(now())))
|
|
16
|
+
return { acquired: false, run: current };
|
|
17
|
+
const claimed = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1,
|
|
18
|
+
status: current.status === "cancelling" ? "cancelling" : "running", runtime: current.status === "cancelling" ? current.runtime : runtime,
|
|
19
|
+
leaseOwner: options.workerId, leaseExpiresAt: new Date(Date.parse(now()) + current.timeoutMs + 30_000).toISOString(),
|
|
20
|
+
attemptCount: current.attemptCount + 1, results: current.status === "cancelling" ? current.results : [], matchesExpectations: null, failure: null, updatedAt: now(),
|
|
21
|
+
});
|
|
22
|
+
await tx.put("reward_check", claimed, current.revision, { parentId: claimed.reward.id, status: claimed.status });
|
|
23
|
+
return { acquired: true, run: claimed };
|
|
24
|
+
});
|
|
25
|
+
if (!claim.acquired)
|
|
26
|
+
return claim.run;
|
|
27
|
+
if (claim.run.status === "cancelling") {
|
|
28
|
+
const cleaned = await executor.cancel({ scope, run: claim.run });
|
|
29
|
+
return finish(scope, claim.run, cleaned ? "cancelled" : "cancelling", null);
|
|
30
|
+
}
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
active.set(key(scope, checkId), controller);
|
|
33
|
+
const timer = setTimeout(() => controller.abort(new Error("reward_check_timeout")), claim.run.timeoutMs);
|
|
34
|
+
try {
|
|
35
|
+
const compiled = await repository.transaction(scope, async (tx) => {
|
|
36
|
+
const draft = await requireLearningRelease(tx, "draft", claim.run.draft);
|
|
37
|
+
if (draft.targetKind !== "reward")
|
|
38
|
+
throw new Error("reward_check_draft_kind_invalid");
|
|
39
|
+
const base = draft.baseRelease ? await requireLearningRelease(tx, "reward", draft.baseRelease) : null;
|
|
40
|
+
const compiled = compileRewardCheck(draft, base);
|
|
41
|
+
if (compiled.snapshotHash !== claim.run.snapshotHash || !sameLearningRef(learningRef(compiled.reward), claim.run.reward) || contentHash(compiled.fixtureRefs) !== contentHash(claim.run.fixtureRefs))
|
|
42
|
+
throw new Error("reward_check_snapshot_mismatch");
|
|
43
|
+
return compiled;
|
|
44
|
+
});
|
|
45
|
+
for (const fixture of compiled.fixtures) {
|
|
46
|
+
controller.signal.throwIfAborted();
|
|
47
|
+
const result = BoundRewardResultSchema.parse(await executor.execute({ ...compiled, scope, run: claim.run, fixture, signal: controller.signal }));
|
|
48
|
+
controller.signal.throwIfAborted();
|
|
49
|
+
if (result.graderId !== compiled.reward.id || !sameLearningRef(result.reward, claim.run.reward) || result.role !== "evaluation")
|
|
50
|
+
throw new Error("reward_check_result_identity_mismatch");
|
|
51
|
+
const retained = await repository.transaction(scope, async (tx) => {
|
|
52
|
+
const current = await requireLearningResource(tx, "reward_check", checkId);
|
|
53
|
+
if (!owns(current, claim.run) || terminal(current))
|
|
54
|
+
return current;
|
|
55
|
+
const updated = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1, results: [...current.results, matchRewardFixture(fixture, result)], updatedAt: now() });
|
|
56
|
+
await tx.put("reward_check", updated, current.revision, { parentId: current.reward.id, status: current.status });
|
|
57
|
+
return updated;
|
|
58
|
+
});
|
|
59
|
+
if (!owns(retained, claim.run) || terminal(retained))
|
|
60
|
+
return retained;
|
|
61
|
+
if (retained.status === "cancelling")
|
|
62
|
+
controller.abort(new Error("reward_check_cancel_requested"));
|
|
63
|
+
}
|
|
64
|
+
controller.signal.throwIfAborted();
|
|
65
|
+
return await finish(scope, claim.run, "completed", null);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (controller.signal.aborted) {
|
|
69
|
+
const cleaned = await executor.cancel({ scope, run: claim.run });
|
|
70
|
+
if (!cleaned)
|
|
71
|
+
return finish(scope, claim.run, "cancelling", "Execution cleanup is awaiting owner confirmation.");
|
|
72
|
+
if (controller.signal.reason instanceof Error && controller.signal.reason.message === "reward_check_cancel_requested")
|
|
73
|
+
return finish(scope, claim.run, "cancelled", null);
|
|
74
|
+
}
|
|
75
|
+
return await finish(scope, claim.run, "failed", error instanceof Error ? error.message : "Reward checks failed.");
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
active.delete(key(scope, checkId));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function owns(current, claim) { return current.leaseOwner === options.workerId && current.attemptCount === claim.attemptCount; }
|
|
83
|
+
async function finish(scope, claim, status, failure) {
|
|
84
|
+
return repository.transaction(scope, async (tx) => {
|
|
85
|
+
const current = await requireLearningResource(tx, "reward_check", claim.id);
|
|
86
|
+
if (!owns(current, claim) || terminal(current))
|
|
87
|
+
return current;
|
|
88
|
+
if (status === "completed" && current.results.length !== current.fixtureRefs.length)
|
|
89
|
+
throw new Error("reward_check_results_incomplete");
|
|
90
|
+
const updated = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1, status, failure,
|
|
91
|
+
matchesExpectations: status === "completed" ? current.results.every(result => result.matchesExpectation) : null,
|
|
92
|
+
leaseOwner: null, leaseExpiresAt: status === "cancelling" ? new Date(Date.parse(now()) + 5_000).toISOString() : null, updatedAt: now(),
|
|
93
|
+
});
|
|
94
|
+
await tx.put("reward_check", updated, current.revision, { parentId: current.reward.id, status: updated.status });
|
|
95
|
+
return updated;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
run,
|
|
100
|
+
requestCancellation(scope, id) { active.get(key(scope, id))?.abort(new Error("reward_check_cancel_requested")); },
|
|
101
|
+
async drain(scope, limit = 20) {
|
|
102
|
+
const candidates = await repository.transaction(scope, async (tx) => {
|
|
103
|
+
const rows = [];
|
|
104
|
+
for (const status of ["queued", "running", "cancelling"])
|
|
105
|
+
rows.push(...(await tx.list("reward_check", { status, limit: Math.max(1, Math.min(100, limit)) })).items);
|
|
106
|
+
return rows;
|
|
107
|
+
});
|
|
108
|
+
const completed = [];
|
|
109
|
+
for (const candidate of candidates)
|
|
110
|
+
completed.push(await run(scope, candidate.id));
|
|
111
|
+
return completed;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
|
|
3
|
+
import { BoundRewardResultSchema, createRewardBinding, executeRewardBinding } from "../rewards.js";
|
|
4
|
+
import { TaskRecordSchema } from "../tasksets.js";
|
|
5
|
+
import { LearningJsonObjectSchema, LearningRevisionRefSchema, learningRef } from "./contracts.js";
|
|
6
|
+
import { authoringNumber, compileRewardAuthoring, parseRewardAuthoringObject } from "./reward-authoring.js";
|
|
7
|
+
import { LearningDomainError } from "./errors.js";
|
|
8
|
+
export const RewardFixtureSchema = z.object({
|
|
9
|
+
id: ReleaseIdSchema, name: z.string().trim().min(1).max(500),
|
|
10
|
+
input: LearningJsonObjectSchema, output: LearningJsonObjectSchema,
|
|
11
|
+
expectedOutput: LearningJsonObjectSchema.nullable(), evaluatorContext: LearningJsonObjectSchema.nullable(),
|
|
12
|
+
artifactRefs: z.array(ReleaseIdSchema).max(100), runtimeEventRefs: z.array(ReleaseIdSchema).max(100),
|
|
13
|
+
infrastructureError: z.string().max(20_000).nullable(),
|
|
14
|
+
expected: z.discriminatedUnion("status", [
|
|
15
|
+
z.object({ status: z.literal("scored"), minimum: z.number().finite(), maximum: z.number().finite(), passed: z.boolean().nullable() }).strict(),
|
|
16
|
+
z.object({ status: z.enum(["pending", "unavailable", "failed"]) }).strict(),
|
|
17
|
+
]),
|
|
18
|
+
}).strict().superRefine((fixture, context) => {
|
|
19
|
+
if (fixture.expected.status === "scored" && fixture.expected.minimum > fixture.expected.maximum)
|
|
20
|
+
context.addIssue({ code: "custom", path: ["expected"], message: "Expected score minimum must not exceed maximum." });
|
|
21
|
+
});
|
|
22
|
+
export const RewardCheckRuntimeSchema = z.object({ id: ReleaseIdSchema, packageVersion: z.string().min(1).max(100), engine: z.string().min(1).max(200) }).strict();
|
|
23
|
+
export const RewardFixtureCheckResultSchema = z.object({
|
|
24
|
+
fixture: z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict(),
|
|
25
|
+
result: BoundRewardResultSchema, matchesExpectation: z.boolean(),
|
|
26
|
+
}).strict();
|
|
27
|
+
export const RewardCheckRunSchema = z.object({
|
|
28
|
+
schemaVersion: z.literal("openpond.rewardCheckRun.v1"), id: ReleaseIdSchema, revision: z.number().int().positive(),
|
|
29
|
+
draft: LearningRevisionRefSchema, reward: LearningRevisionRefSchema, snapshotHash: ReleaseHashSchema,
|
|
30
|
+
fixtureRefs: z.array(z.object({ id: ReleaseIdSchema, contentHash: ReleaseHashSchema }).strict()).min(1).max(50),
|
|
31
|
+
status: z.enum(["queued", "running", "completed", "failed", "cancelling", "cancelled"]),
|
|
32
|
+
runtime: RewardCheckRuntimeSchema.nullable(), results: z.array(RewardFixtureCheckResultSchema).max(50),
|
|
33
|
+
matchesExpectations: z.boolean().nullable(), failure: z.string().max(20_000).nullable(),
|
|
34
|
+
timeoutMs: z.number().int().min(100).max(300_000), maximumSpendUsd: z.number().nonnegative().max(1_000),
|
|
35
|
+
leaseOwner: ReleaseIdSchema.nullable(), leaseExpiresAt: ReleaseTimestampSchema.nullable(), attemptCount: z.number().int().nonnegative(),
|
|
36
|
+
createdAt: ReleaseTimestampSchema, updatedAt: ReleaseTimestampSchema,
|
|
37
|
+
}).strict();
|
|
38
|
+
export function compileRewardFixtures(fields) {
|
|
39
|
+
if (!fields.length || fields.length > 50)
|
|
40
|
+
throw new LearningDomainError("reward_check_fixtures_required", 422, "Add between one and 50 fixtures before checking this Reward.");
|
|
41
|
+
if (new Set(fields.map(field => field.id)).size !== fields.length)
|
|
42
|
+
throw new LearningDomainError("reward_fixture_id_duplicate", 422);
|
|
43
|
+
return fields.map(field => RewardFixtureSchema.parse({
|
|
44
|
+
id: field.id, name: field.name,
|
|
45
|
+
input: parseRewardAuthoringObject(field.input, `${field.name}: input`), output: parseRewardAuthoringObject(field.output, `${field.name}: output`),
|
|
46
|
+
expectedOutput: field.expectedOutput.trim() ? parseRewardAuthoringObject(field.expectedOutput, `${field.name}: expected answer`) : null,
|
|
47
|
+
evaluatorContext: field.evaluatorContext.trim() ? parseRewardAuthoringObject(field.evaluatorContext, `${field.name}: evaluator context`) : null,
|
|
48
|
+
artifactRefs: field.artifactRefs.map(value => value.trim()).filter(Boolean), runtimeEventRefs: field.runtimeEventRefs.map(value => value.trim()).filter(Boolean), infrastructureError: field.infrastructureError || null,
|
|
49
|
+
expected: field.expectedStatus === "scored" ? { status: field.expectedStatus, minimum: authoringNumber(field.minimumScore, `${field.name}: minimum score`), maximum: authoringNumber(field.maximumScore, `${field.name}: maximum score`), passed: field.expectedPassed === "any" ? null : field.expectedPassed === "true" } : { status: field.expectedStatus },
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
export function compileRewardCheck(draft, base) {
|
|
53
|
+
const compiled = compileRewardAuthoring({ id: draft.targetId, fields: draft.fields, base });
|
|
54
|
+
const fixtures = compileRewardFixtures(draft.fields.fixtures ?? []);
|
|
55
|
+
const fixtureRefs = fixtures.map(fixture => ({ id: fixture.id, contentHash: contentHash(fixture) }));
|
|
56
|
+
return { ...compiled, fixtures, fixtureRefs, snapshotHash: contentHash({ reward: compiled.reward, fixtures }) };
|
|
57
|
+
}
|
|
58
|
+
/** Fixture data is authored test data, never an owner-recorded production attempt. */
|
|
59
|
+
export async function executeRewardFixture(input) {
|
|
60
|
+
const fixture = RewardFixtureSchema.parse(input.fixture);
|
|
61
|
+
const task = TaskRecordSchema.parse({
|
|
62
|
+
id: fixture.id, clusterKey: fixture.id, split: "validation", input: fixture.input,
|
|
63
|
+
expectedOutput: fixture.expectedOutput, privilegedContextRef: null,
|
|
64
|
+
});
|
|
65
|
+
const binding = createRewardBinding({
|
|
66
|
+
schemaVersion: "openpond.rewardBinding.v1", id: `fixture-binding-${contentHash(learningRef(input.reward))}`, revision: 1,
|
|
67
|
+
sources: [{ graderId: input.reward.id, reward: learningRef(input.reward), role: "evaluation", normalization: input.reward.rawScore.minimum === 0 && input.reward.rawScore.maximum === 1 ? { kind: "identity" } : { kind: "linear", minimum: input.reward.rawScore.minimum, maximum: input.reward.rawScore.maximum, direction: "higher" }, weight: 1, required: true, hardGate: false, privileged: true, fixtureRefs: [] }],
|
|
68
|
+
aggregation: "weighted_mean", unscorable: "exclude_optional_require_all_required",
|
|
69
|
+
}, [input.reward]);
|
|
70
|
+
const composition = await executeRewardBinding({ binding, rewards: [input.reward], task,
|
|
71
|
+
evidence: { output: fixture.output, artifactRefs: fixture.artifactRefs, runtimeEventRefs: fixture.runtimeEventRefs, infrastructureError: fixture.infrastructureError },
|
|
72
|
+
signal: input.signal, customVerifier: input.customVerifier, modelJudge: input.modelJudge,
|
|
73
|
+
});
|
|
74
|
+
return composition.results[0];
|
|
75
|
+
}
|
|
76
|
+
export function matchRewardFixture(fixture, result) {
|
|
77
|
+
const expected = fixture.expected;
|
|
78
|
+
const matchesExpectation = result.status === expected.status && (expected.status !== "scored"
|
|
79
|
+
|| (result.rawScore !== null && result.rawScore >= expected.minimum && result.rawScore <= expected.maximum && (expected.passed === null || expected.passed === result.passed)));
|
|
80
|
+
return RewardFixtureCheckResultSchema.parse({ fixture: { id: fixture.id, contentHash: contentHash(fixture) }, result, matchesExpectation });
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { verifyLearningTextAsset } from "./assets.js";
|
|
2
|
+
import { executeRewardFixture } from "./reward-checks.js";
|
|
3
|
+
/** The supplied interpreter must settle only after its worker/process exits. */
|
|
4
|
+
export function createIsolatedRewardFixtureExecutor(options) {
|
|
5
|
+
return {
|
|
6
|
+
runtime: options.runtime,
|
|
7
|
+
execute(input) {
|
|
8
|
+
return executeRewardFixture({
|
|
9
|
+
reward: input.reward, fixture: input.fixture, signal: input.signal,
|
|
10
|
+
customVerifier: async ({ grader, task, evidence }) => {
|
|
11
|
+
const asset = input.assets.find(asset => asset.id === grader.verifierRef.id);
|
|
12
|
+
if (!asset)
|
|
13
|
+
throw new Error("reward_check_source_missing");
|
|
14
|
+
const source = verifyLearningTextAsset(asset, grader.verifierRef);
|
|
15
|
+
const result = await options.executeJavaScript({
|
|
16
|
+
source, exportName: grader.exportName, signal: input.signal,
|
|
17
|
+
timeoutMs: Math.min(grader.timeoutMs, input.run.timeoutMs),
|
|
18
|
+
value: { task, attempt: evidence, input: task.input, output: evidence.output, expectedOutput: task.expectedOutput,
|
|
19
|
+
evaluatorContext: input.fixture.evaluatorContext, infrastructureError: evidence.infrastructureError ?? null },
|
|
20
|
+
});
|
|
21
|
+
return { score: result.score, passed: result.passed, rewardEligible: grader.rewardEligible, failureClass: null,
|
|
22
|
+
feedback: [result.feedback], visibleEvidenceRefs: [], privilegedEvidenceRefs: result.evidenceRefs };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
// No remote allocation; executeJavaScript's settlement contract confirms cleanup.
|
|
27
|
+
async cancel() { return true; },
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/learning/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { saveAuthoringDraft, archiveAuthoringDraft, finalizeAuthoringDraft } from "./authoring-service.js";
|
|
2
|
+
import { queueRewardCheck, cancelRewardCheck } from "./reward-check-service.js";
|
|
2
3
|
import { LearningDomainError } from "./errors.js";
|
|
3
4
|
import { contentHash } from "@openpond/harness";
|
|
4
5
|
import { createRewardBinding, createRewardRelease, resolveBoundRewards } from "../rewards.js";
|
|
@@ -28,6 +29,12 @@ export function createLearningService(repository, options = {}) {
|
|
|
28
29
|
}
|
|
29
30
|
let pointers;
|
|
30
31
|
switch (input.action) {
|
|
32
|
+
case "queue_reward_check":
|
|
33
|
+
pointers = [await queueRewardCheck(transaction, input, operationId, now())];
|
|
34
|
+
break;
|
|
35
|
+
case "cancel_reward_check":
|
|
36
|
+
pointers = [await cancelRewardCheck(transaction, input, now())];
|
|
37
|
+
break;
|
|
31
38
|
case "save_draft":
|
|
32
39
|
pointers = [await saveAuthoringDraft(transaction, input, now())];
|
|
33
40
|
break;
|
package/dist/rewards.js
CHANGED
|
@@ -21,7 +21,10 @@ export const RewardReleaseContentSchema = z.object({
|
|
|
21
21
|
implementation: RewardImplementationSchema,
|
|
22
22
|
rawScore: z.object({ minimum: z.number().finite(), maximum: z.number().finite() }).strict(),
|
|
23
23
|
assets: z.array(ImmutableAssetRefSchema).max(1_000),
|
|
24
|
+
fixtureSetRef: ImmutableAssetRefSchema.optional(),
|
|
24
25
|
}).strict().superRefine((reward, context) => {
|
|
26
|
+
if (reward.fixtureSetRef && (reward.fixtureSetRef.visibility !== "verifier" || !reward.assets.some(asset => contentHash(asset) === contentHash(reward.fixtureSetRef))))
|
|
27
|
+
context.addIssue({ code: "custom", path: ["fixtureSetRef"], message: "Reward fixtures must be included as a private verifier asset." });
|
|
25
28
|
if (reward.rawScore.minimum >= reward.rawScore.maximum)
|
|
26
29
|
context.addIssue({ code: "custom", path: ["rawScore"], message: "A raw score contract requires minimum < maximum." });
|
|
27
30
|
if (reward.implementation.kind !== "learned_model" && (reward.rawScore.minimum !== 0 || reward.rawScore.maximum !== 1))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assertBoundedTaskJson } from "./task-schema.js";
|
|
3
|
+
const integer = z.string().regex(/^(0|-?[1-9][0-9]*)$/).max(20).refine(value => {
|
|
4
|
+
if (value.length > 20 || !/^(0|-?[1-9][0-9]*)$/.test(value))
|
|
5
|
+
return false;
|
|
6
|
+
const parsed = BigInt(value);
|
|
7
|
+
return parsed >= -(1n << 63n) && parsed < (1n << 63n);
|
|
8
|
+
}, "SQL integers must fit signed 64-bit storage.");
|
|
9
|
+
export const SqlValueSchema = z.union([
|
|
10
|
+
z.null(), z.string().max(262_144),
|
|
11
|
+
z.number().finite(),
|
|
12
|
+
z.object({ integer }).strict(),
|
|
13
|
+
z.object({ blobBase64: z.string().max(349_528).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/) }).strict(),
|
|
14
|
+
]);
|
|
15
|
+
const identifier = z.string().min(1).max(128).refine(value => !value.includes("\0"));
|
|
16
|
+
export const SqlSnapshotSchema = z.object({
|
|
17
|
+
tables: z.array(z.object({
|
|
18
|
+
name: identifier,
|
|
19
|
+
columns: z.array(z.object({ name: identifier, type: z.enum(["INTEGER", "REAL", "TEXT", "BLOB"]) }).strict()).min(1).max(128),
|
|
20
|
+
rows: z.array(z.array(SqlValueSchema).max(128)).max(4_096),
|
|
21
|
+
}).strict()).max(64),
|
|
22
|
+
}).strict().superRefine((snapshot, context) => {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const [index, table] of snapshot.tables.entries()) {
|
|
25
|
+
const name = table.name.toLowerCase();
|
|
26
|
+
if (names.has(name) || name.startsWith("sqlite_"))
|
|
27
|
+
context.addIssue({ code: "custom", path: ["tables", index, "name"], message: "Table names must be unique and outside SQLite's reserved namespace." });
|
|
28
|
+
names.add(name);
|
|
29
|
+
if (new Set(table.columns.map(column => column.name.toLowerCase())).size !== table.columns.length)
|
|
30
|
+
context.addIssue({ code: "custom", path: ["tables", index, "columns"], message: "Column names must be unique." });
|
|
31
|
+
if (table.rows.some(row => row.length !== table.columns.length))
|
|
32
|
+
context.addIssue({ code: "custom", path: ["tables", index, "rows"], message: "Each row must match the declared columns." });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export const SqlExecutionRequestSchema = z.object({
|
|
36
|
+
schemaVersion: z.literal("openpond.sqlExecution.v1"),
|
|
37
|
+
snapshot: SqlSnapshotSchema,
|
|
38
|
+
sql: z.string().min(1).max(65_536).refine(value => !value.includes("\0")),
|
|
39
|
+
maxRows: z.number().int().min(1).max(4_096),
|
|
40
|
+
maxResultBytes: z.number().int().min(256).max(262_144),
|
|
41
|
+
}).strict();
|
|
42
|
+
export const SqlExecutionResultSchema = z.discriminatedUnion("status", [
|
|
43
|
+
z.object({ status: z.literal("completed"), columns: z.array(z.string()).max(128), rows: z.array(z.array(SqlValueSchema).max(128)).max(4_096) }).strict(),
|
|
44
|
+
z.object({ status: z.literal("rejected"), code: z.enum(["invalid_query", "multiple_statements", "result_too_large", "memory_limit"]) }).strict(),
|
|
45
|
+
]);
|
|
46
|
+
export function assertSqlExecutionRequest(value) {
|
|
47
|
+
assertBoundedTaskJson(value, 1_048_576);
|
|
48
|
+
const parsed = SqlExecutionRequestSchema.parse(value);
|
|
49
|
+
if (new TextEncoder().encode(parsed.sql).byteLength > 65_536)
|
|
50
|
+
throw new Error("sql_source_too_large");
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|