@springbrand/eval-harness 0.1.0-alpha.0 → 0.1.0-alpha.1
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/README.md +63 -4
- package/dist/index.d.ts +57 -3
- package/dist/index.js +278 -180
- package/package.json +11 -16
package/README.md
CHANGED
|
@@ -38,11 +38,30 @@ independent Eval SP deployment; the shared SP dev API and database are not used.
|
|
|
38
38
|
|
|
39
39
|
The initialization body includes the Data sample and, only when referenced by
|
|
40
40
|
that sample, `initialSession: {bundleId, sessionId, context?: {format, content}}`
|
|
41
|
-
and/or `initialMaterials: {bundleId, materialId, files}`. `content` is canonical
|
|
41
|
+
and/or `initialMaterials: [{bundleId, materialId, files}]`. `content` is canonical
|
|
42
42
|
Session JSON or JSONL; each material file supplies its path, media type and
|
|
43
|
-
base64 bytes.
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
base64 bytes. The material list must match the sample's `initialMaterials`
|
|
44
|
+
references position by position, without duplicates. Supplied Session context is
|
|
45
|
+
imported into the Chat before the Prompt and only files are written to SpaceDO,
|
|
46
|
+
each material under its own `evalMaterialRoot`. A sample may specify either
|
|
47
|
+
source, both, or neither. There is no Dataset-level source binding. An Evaluator
|
|
48
|
+
initialization must carry `evaluationOutputSchema: {content}`, written as UTF-8
|
|
49
|
+
`application/schema+json` to `EVAL_OUTPUT_SCHEMA_PATH`
|
|
50
|
+
(`/evaluation-output.schema.json`); a Subject Chat rejects it. Evaluators do not
|
|
51
|
+
import an initial Session. Schema validity and result validation belong to Eval
|
|
52
|
+
Platform; SP preserves the supplied text. `evalMaterialFilesSchema`
|
|
53
|
+
validates one material's `EvalMaterialFile` list: at most `EVAL_MATERIAL_MAX_FILES`, with unique
|
|
54
|
+
paths. One initialization carries at most `EVAL_INITIAL_FILE_MAX_BYTES` per file
|
|
55
|
+
and `EVAL_INITIALIZATION_MAX_BYTES` in total; size violations are Zod `too_big`
|
|
56
|
+
issues. `evalMaterialFilesByteSize` returns one material's decoded bytes exactly as
|
|
57
|
+
that total counts them. `readEvalCanonicalContext` accepts at most
|
|
58
|
+
`EVAL_SESSION_CONTENT_MAX_CHARS` characters and `EVAL_SESSION_MAX_ENTRIES`
|
|
59
|
+
entries. `EVAL_INITIALIZATION_BODY_MAX_BYTES` bounds transport JSON, accounting
|
|
60
|
+
for base64 material bytes, escaped Session or Schema text, and JSON framing.
|
|
61
|
+
Eval consumes these constants instead of copying the values. Its result is
|
|
62
|
+
`evalInitializationResultSchema`: `{manifestHash, target, materials:
|
|
63
|
+
[{bundleId, materialId, rootPath}], evaluationOutputSchemaPath?}` in request
|
|
64
|
+
order; replaying the same request returns the stored result.
|
|
46
65
|
|
|
47
66
|
An Evaluator is an ordinary Eval Chat with its own Session, Submission and
|
|
48
67
|
Space; its creation facts add only `evaluationId`. It never reads or writes the
|
|
@@ -61,6 +80,46 @@ hash, writes `/evidence/<path>` and returns `{path, size, sha256}`. Only an
|
|
|
61
80
|
Evaluator imports; an identical repeat is a no-op, different content is a 409,
|
|
62
81
|
and imports stop with the Chat's stop latch.
|
|
63
82
|
|
|
83
|
+
The Evaluator Workspace layout is one public contract, exported by this package:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
/initial-materials/{bundleId}/{materialId}/... # Evaluator's own materials
|
|
87
|
+
/evidence/subject-inputs/{bundleId}/{materialId}/... # Subject's original input materials
|
|
88
|
+
/evidence/artifacts/{original-relative-path} # Subject's produced files
|
|
89
|
+
/evidence/trajectory/events-000001.jsonl # Complete raw trajectory, ordered parts
|
|
90
|
+
/evidence/manifest.json # Evidence bundle inventory and provenance
|
|
91
|
+
/evaluation-output.schema.json # Evaluation output format
|
|
92
|
+
/output/result.json # Evaluator's scored result
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`EVAL_EVIDENCE_PATHS` names the bundle-relative directories. The import route takes
|
|
96
|
+
paths **without** the `/evidence` prefix: `/subject-inputs/...`, `/artifacts/...`,
|
|
97
|
+
`/trajectory/...` or `/manifest.json`. Other paths are rejected. The exported
|
|
98
|
+
`evalSubjectInputEvidencePath`, `evalArtifactEvidencePath` and
|
|
99
|
+
`evalTrajectoryEvidencePath` build those import paths; `evalEvidencePath` turns
|
|
100
|
+
an import path into its absolute Workspace path. IDs are encoded as individual
|
|
101
|
+
path segments and original file hierarchy is preserved, so equal basenames in
|
|
102
|
+
different materials or evidence categories cannot overwrite each other.
|
|
103
|
+
|
|
104
|
+
Eval Platform copies Subject inputs from the original immutable Material bundles
|
|
105
|
+
referenced by this Sampling, not from the Subject's potentially edited working
|
|
106
|
+
files. SP continues to exclude these inputs from artifact exports. Eval Platform
|
|
107
|
+
collects **every** trajectory page until `nextAfterSeq` is null, retaining raw
|
|
108
|
+
`{ingestSeq,event}` records in order; it splits oversized JSONL at event boundaries
|
|
109
|
+
so every part fits `EVAL_EVIDENCE_FILE_MAX_BYTES`. Eval Platform owns the frozen
|
|
110
|
+
bundle, manifest body, file hashes and preparation retries. It imports all files,
|
|
111
|
+
verifies their receipts and uploads `manifest.json` last, then submits its Prompt
|
|
112
|
+
with explicit evidence, Schema and result paths.
|
|
113
|
+
|
|
114
|
+
SP requires completed initialization before evidence import, and a nonempty
|
|
115
|
+
evidence manifest before an Evaluator's first Submission. Input writes and
|
|
116
|
+
Submission admission are serialized; the first reserved Submission durably closes
|
|
117
|
+
evidence imports, including after a DO restart. Replaying initialization returns
|
|
118
|
+
its saved result without restoring files. Stop closes admission immediately and
|
|
119
|
+
waits for accepted input writes and submissions. Evidence, initial materials and
|
|
120
|
+
the fixed Schema input path are never exported as produced artifacts. SP does not
|
|
121
|
+
interpret the manifest or implement Eval Platform's bundle assembly or scoring.
|
|
122
|
+
|
|
64
123
|
Eval Platform writes the Evaluator instructions into the Prompt; SP submits it
|
|
65
124
|
unchanged. The Evaluator's `output` adds `structuredOutput: {path, content,
|
|
66
125
|
contentHash}` with the raw UTF-8 text of `/output/result.json` in its own Space
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export declare const EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
|
|
|
4
4
|
export declare const evalEnsureChatSchema: z.ZodObject<{
|
|
5
5
|
chatId: z.ZodUUID;
|
|
6
6
|
role: z.ZodEnum<{
|
|
7
|
-
evaluator: "evaluator";
|
|
8
7
|
subject: "subject";
|
|
8
|
+
evaluator: "evaluator";
|
|
9
9
|
}>;
|
|
10
10
|
framework: z.ZodLiteral<"sp">;
|
|
11
11
|
samplingId: z.ZodString;
|
|
@@ -19,11 +19,29 @@ export declare const evalEnsureChatSchema: z.ZodObject<{
|
|
|
19
19
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
20
20
|
}, z.core.$strict>>;
|
|
21
21
|
}, z.core.$strict>;
|
|
22
|
-
export declare const
|
|
22
|
+
export declare const EVAL_INITIAL_FILE_MAX_BYTES = 5000000;
|
|
23
|
+
export declare const EVAL_INITIALIZATION_MAX_BYTES = 20000000;
|
|
24
|
+
export declare const EVAL_MATERIAL_MAX_FILES = 500;
|
|
25
|
+
export declare const EVAL_SESSION_CONTENT_MAX_CHARS = 2000000;
|
|
26
|
+
export declare const EVAL_SESSION_MAX_ENTRIES = 10000;
|
|
27
|
+
export declare const EVAL_OUTPUT_SCHEMA_PATH = "/evaluation-output.schema.json";
|
|
28
|
+
export declare const EVAL_INITIAL_MATERIALS_ROOT = "/initial-materials";
|
|
29
|
+
/** Subject Session JSON and Evaluator Schema JSON can expand to six bytes per character. */
|
|
30
|
+
export declare const EVAL_INITIALIZATION_BODY_MAX_BYTES: number;
|
|
31
|
+
/** Decoded bytes of validated Material files, counted exactly as the initialization budget counts them. */
|
|
32
|
+
export declare const evalMaterialFilesByteSize: (files: readonly {
|
|
33
|
+
readonly contentBase64: string;
|
|
34
|
+
}[]) => number;
|
|
35
|
+
declare const evalMaterialFileSchema: z.ZodObject<{
|
|
23
36
|
path: z.ZodString;
|
|
24
37
|
mediaType: z.ZodString;
|
|
25
38
|
contentBase64: z.ZodBase64;
|
|
26
39
|
}, z.core.$strict>;
|
|
40
|
+
export declare const evalMaterialFilesSchema: z.ZodArray<z.ZodObject<{
|
|
41
|
+
path: z.ZodString;
|
|
42
|
+
mediaType: z.ZodString;
|
|
43
|
+
contentBase64: z.ZodBase64;
|
|
44
|
+
}, z.core.$strict>>;
|
|
27
45
|
export declare const evalInitialContextSchema: z.ZodObject<{
|
|
28
46
|
format: z.ZodEnum<{
|
|
29
47
|
json: "json";
|
|
@@ -47,7 +65,7 @@ export declare const evalInitializationSchema: z.ZodObject<{
|
|
|
47
65
|
content: z.ZodString;
|
|
48
66
|
}, z.core.$strict>>;
|
|
49
67
|
}, z.core.$strict>>;
|
|
50
|
-
initialMaterials: z.ZodOptional<z.ZodObject<{
|
|
68
|
+
initialMaterials: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
51
69
|
bundleId: z.ZodString;
|
|
52
70
|
materialId: z.ZodString;
|
|
53
71
|
files: z.ZodArray<z.ZodObject<{
|
|
@@ -55,8 +73,21 @@ export declare const evalInitializationSchema: z.ZodObject<{
|
|
|
55
73
|
mediaType: z.ZodString;
|
|
56
74
|
contentBase64: z.ZodBase64;
|
|
57
75
|
}, z.core.$strict>>;
|
|
76
|
+
}, z.core.$strict>>>;
|
|
77
|
+
evaluationOutputSchema: z.ZodOptional<z.ZodObject<{
|
|
78
|
+
content: z.ZodString;
|
|
58
79
|
}, z.core.$strict>>;
|
|
59
80
|
}, z.core.$strict>;
|
|
81
|
+
export declare const evalInitializationResultSchema: z.ZodObject<{
|
|
82
|
+
manifestHash: z.ZodString;
|
|
83
|
+
target: z.ZodLiteral<"workspace">;
|
|
84
|
+
materials: z.ZodArray<z.ZodObject<{
|
|
85
|
+
bundleId: z.ZodString;
|
|
86
|
+
materialId: z.ZodString;
|
|
87
|
+
rootPath: z.ZodString;
|
|
88
|
+
}, z.core.$strict>>;
|
|
89
|
+
evaluationOutputSchemaPath: z.ZodExactOptional<z.ZodLiteral<"/evaluation-output.schema.json">>;
|
|
90
|
+
}, z.core.$strict>;
|
|
60
91
|
export declare const evalSubmitSchema: z.ZodObject<{
|
|
61
92
|
idempotencyKey: z.ZodString;
|
|
62
93
|
prompt: z.ZodString;
|
|
@@ -66,7 +97,9 @@ export declare const evalSubmitSchema: z.ZodObject<{
|
|
|
66
97
|
}, z.core.$strict>>;
|
|
67
98
|
}, z.core.$strict>;
|
|
68
99
|
export type EvalEnsureChat = z.output<typeof evalEnsureChatSchema>;
|
|
100
|
+
export type EvalMaterialFile = z.output<typeof evalMaterialFileSchema>;
|
|
69
101
|
export type EvalInitialization = z.output<typeof evalInitializationSchema>;
|
|
102
|
+
export type EvalInitializationResult = z.output<typeof evalInitializationResultSchema>;
|
|
70
103
|
export type EvalSubmit = z.output<typeof evalSubmitSchema>;
|
|
71
104
|
export type EvalSubmissionReceipt = {
|
|
72
105
|
readonly submissionId: string;
|
|
@@ -135,6 +168,13 @@ export declare function evalMaterialRoot(bundleId: string, materialId: string):
|
|
|
135
168
|
export declare function evalMaterialPath(root: string, path: string): string;
|
|
136
169
|
/** Where imported evidence lives in the Evaluator's own Space. */
|
|
137
170
|
export declare const EVAL_EVIDENCE_ROOT = "/evidence";
|
|
171
|
+
/** Bundle-relative paths, used by the import route; evalEvidencePath returns their Workspace paths. */
|
|
172
|
+
export declare const EVAL_EVIDENCE_PATHS: {
|
|
173
|
+
readonly subjectInputs: "/subject-inputs";
|
|
174
|
+
readonly artifacts: "/artifacts";
|
|
175
|
+
readonly trajectory: "/trajectory";
|
|
176
|
+
readonly manifest: "/manifest.json";
|
|
177
|
+
};
|
|
138
178
|
/** One exported or imported file crosses a single Durable Object RPC as bytes. */
|
|
139
179
|
export declare const EVAL_EVIDENCE_FILE_MAX_BYTES = 25000000;
|
|
140
180
|
export declare const EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
|
|
@@ -169,6 +209,19 @@ export declare function readEvalTrajectoryPageQuery(params: URLSearchParams): {
|
|
|
169
209
|
};
|
|
170
210
|
/** A Space path for one evidence file; `relative` keeps the bundle's own layout below `/evidence`. */
|
|
171
211
|
export declare function evalEvidencePath(relative: string): string;
|
|
212
|
+
/** Original Subject inputs stay separate from produced artifacts and the Evaluator's own materials. */
|
|
213
|
+
export declare function evalSubjectInputEvidencePath(bundleId: string, materialId: string, path: string): string;
|
|
214
|
+
export declare function evalArtifactEvidencePath(path: string): string;
|
|
215
|
+
/** Positive part numbers preserve the complete raw JSONL trajectory's order. */
|
|
216
|
+
export declare function evalTrajectoryEvidencePath(part: number): string;
|
|
217
|
+
/** One raw evidence file imported into the Evaluator's own Space. */
|
|
218
|
+
export type EvalEvidenceFileInput = {
|
|
219
|
+
/** Bundle-relative path under one of EVAL_EVIDENCE_PATHS, without the /evidence prefix. */
|
|
220
|
+
readonly path: string;
|
|
221
|
+
readonly mediaType: string;
|
|
222
|
+
readonly sha256: string;
|
|
223
|
+
readonly bytes: Uint8Array;
|
|
224
|
+
};
|
|
172
225
|
/** The receipt for one imported file; identical repeats return the same receipt. */
|
|
173
226
|
export type EvalEvidenceFileReceipt = {
|
|
174
227
|
readonly path: string;
|
|
@@ -178,3 +231,4 @@ export type EvalEvidenceFileReceipt = {
|
|
|
178
231
|
export declare function isEvalSha256(value: string | null): value is string;
|
|
179
232
|
/** Compare creation and operation facts without depending on JSON property order. */
|
|
180
233
|
export declare function stableEvalFacts(value: unknown): string;
|
|
234
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,196 +1,294 @@
|
|
|
1
|
+
// packages/eval-harness/src/index.ts
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
|
|
4
|
+
// packages/utils/src/json.ts
|
|
5
|
+
function canonicalJsonStringify(value) {
|
|
6
|
+
return JSON.stringify(
|
|
7
|
+
value,
|
|
8
|
+
(_key, item) => item !== null && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// packages/utils/src/content-hash.ts
|
|
13
|
+
async function sha256Hex(value) {
|
|
14
|
+
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
15
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
16
|
+
new Uint8Array(buffer).set(bytes);
|
|
17
|
+
const digest = await crypto.subtle.digest("SHA-256", buffer);
|
|
18
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/eval-harness/src/index.ts
|
|
22
|
+
var EVAL_SERVICE_HEADER = "X-Eval-Service-Token";
|
|
23
|
+
var EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
|
|
24
|
+
var id = z.string().trim().min(1);
|
|
25
|
+
var uuid = z.uuid();
|
|
26
|
+
var evalEnsureChatSchema = z.strictObject({
|
|
27
|
+
chatId: uuid,
|
|
28
|
+
role: z.enum(["subject", "evaluator"]),
|
|
29
|
+
framework: z.literal("sp"),
|
|
30
|
+
samplingId: id,
|
|
31
|
+
projectId: id,
|
|
32
|
+
agentConfig: z.strictObject({ ownerUserId: uuid, userAgentId: uuid }),
|
|
33
|
+
/** Only an Evaluator carries it; the Evaluator still owns an ordinary Chat and its own Space. */
|
|
34
|
+
evaluationId: id.optional(),
|
|
35
|
+
runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional()
|
|
16
36
|
}).superRefine((value, context) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
if (value.role === "evaluator" !== (value.evaluationId !== void 0)) {
|
|
38
|
+
context.addIssue({ code: "custom", path: ["evaluationId"], message: "Evaluation ID must match role" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
var EVAL_INITIAL_FILE_MAX_BYTES = 5e6;
|
|
42
|
+
var EVAL_INITIALIZATION_MAX_BYTES = 2e7;
|
|
43
|
+
var EVAL_MATERIAL_MAX_FILES = 500;
|
|
44
|
+
var EVAL_SESSION_CONTENT_MAX_CHARS = 2e6;
|
|
45
|
+
var EVAL_SESSION_MAX_ENTRIES = 1e4;
|
|
46
|
+
var EVAL_OUTPUT_SCHEMA_PATH = "/evaluation-output.schema.json";
|
|
47
|
+
var EVAL_INITIAL_MATERIALS_ROOT = "/initial-materials";
|
|
48
|
+
var EVAL_INITIALIZATION_BODY_MAX_BYTES = Math.max(
|
|
49
|
+
Math.ceil(EVAL_INITIALIZATION_MAX_BYTES * 4 / 3) + EVAL_SESSION_CONTENT_MAX_CHARS * 6,
|
|
50
|
+
Math.ceil((EVAL_INITIALIZATION_MAX_BYTES - EVAL_INITIAL_FILE_MAX_BYTES) * 4 / 3) + EVAL_INITIAL_FILE_MAX_BYTES * 6
|
|
51
|
+
) + 1e6;
|
|
52
|
+
var base64Bytes = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
|
|
53
|
+
var utf8Bytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
54
|
+
var evalMaterialFilesByteSize = (files) => files.reduce((sum, file) => sum + base64Bytes(file.contentBase64), 0);
|
|
55
|
+
var tooBig = (context, maximum, message, path = []) => context.addIssue({ code: "too_big", origin: "file", maximum, inclusive: true, path, message });
|
|
56
|
+
var maxFileBytes = (bytes) => (value, context) => {
|
|
57
|
+
if (bytes(value) > EVAL_INITIAL_FILE_MAX_BYTES) tooBig(context, EVAL_INITIAL_FILE_MAX_BYTES, "File is too large");
|
|
58
|
+
};
|
|
59
|
+
var evalMaterialFileSchema = z.strictObject({
|
|
60
|
+
path: z.string().refine((path) => evalPathParts(path) !== null, "Invalid initial material path"),
|
|
61
|
+
mediaType: id,
|
|
62
|
+
contentBase64: z.base64().superRefine(maxFileBytes(base64Bytes))
|
|
20
63
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
var evalMaterialFilesSchema = z.array(evalMaterialFileSchema).max(EVAL_MATERIAL_MAX_FILES).superRefine((files, context) => {
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
files.forEach(({ path }, index) => {
|
|
67
|
+
if (seen.has(path)) context.addIssue({ code: "custom", path: [index, "path"], message: "Duplicate material path" });
|
|
68
|
+
seen.add(path);
|
|
69
|
+
});
|
|
25
70
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
71
|
+
var evalInitialMaterialSchema = z.strictObject({
|
|
72
|
+
bundleId: id,
|
|
73
|
+
materialId: id,
|
|
74
|
+
files: evalMaterialFilesSchema
|
|
29
75
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
var evalInitialContextSchema = z.strictObject({
|
|
77
|
+
format: z.enum(["json", "jsonl"]),
|
|
78
|
+
content: z.string().min(1)
|
|
79
|
+
});
|
|
80
|
+
var evalInitializationSchema = z.strictObject({
|
|
81
|
+
idempotencyKey: id,
|
|
82
|
+
data: z.record(z.string(), z.unknown()).and(z.object({ userPrompt: z.string().trim().min(1) })),
|
|
83
|
+
initialSession: z.strictObject({
|
|
84
|
+
bundleId: id,
|
|
85
|
+
sessionId: id,
|
|
86
|
+
context: evalInitialContextSchema.optional()
|
|
87
|
+
}).optional(),
|
|
88
|
+
initialMaterials: z.array(evalInitialMaterialSchema).min(1).optional(),
|
|
89
|
+
evaluationOutputSchema: z.strictObject({
|
|
90
|
+
content: z.string().min(1).superRefine(maxFileBytes(utf8Bytes))
|
|
91
|
+
}).optional()
|
|
43
92
|
}).superRefine((value, context) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
const session = value.data.initialSession;
|
|
94
|
+
const refs = value.data.initialMaterials;
|
|
95
|
+
const materials = value.initialMaterials;
|
|
96
|
+
if (session ? !value.initialSession || session.bundleId !== value.initialSession.bundleId || session.sessionId !== value.initialSession.sessionId : value.initialSession !== void 0) {
|
|
97
|
+
context.addIssue({ code: "custom", path: ["initialSession"], message: "Session content must match the Data reference" });
|
|
98
|
+
}
|
|
99
|
+
if (refs === void 0 ? materials !== void 0 : !Array.isArray(refs) || refs.length !== materials?.length || materials.some((material, index) => {
|
|
100
|
+
const ref = refs[index];
|
|
101
|
+
return ref?.bundleId !== material.bundleId || ref?.materialId !== material.materialId;
|
|
102
|
+
})) {
|
|
103
|
+
context.addIssue({ code: "custom", path: ["initialMaterials"], message: "Material content must match the Data reference" });
|
|
104
|
+
}
|
|
105
|
+
const seen = /* @__PURE__ */ new Set();
|
|
106
|
+
materials?.forEach(({ bundleId, materialId }, index) => {
|
|
107
|
+
const key = JSON.stringify([bundleId, materialId]);
|
|
108
|
+
if (seen.has(key)) context.addIssue({ code: "custom", path: ["initialMaterials", index], message: "Duplicate material" });
|
|
109
|
+
seen.add(key);
|
|
110
|
+
});
|
|
111
|
+
const total = (materials ?? []).reduce(
|
|
112
|
+
(sum, material) => sum + evalMaterialFilesByteSize(material.files),
|
|
113
|
+
value.evaluationOutputSchema ? utf8Bytes(value.evaluationOutputSchema.content) : 0
|
|
114
|
+
);
|
|
115
|
+
if (total > EVAL_INITIALIZATION_MAX_BYTES) {
|
|
116
|
+
tooBig(context, EVAL_INITIALIZATION_MAX_BYTES, "Initialization files are too large");
|
|
117
|
+
}
|
|
54
118
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
var evalInitializationResultSchema = z.strictObject({
|
|
120
|
+
manifestHash: id,
|
|
121
|
+
target: z.literal("workspace"),
|
|
122
|
+
materials: z.array(z.strictObject({ bundleId: id, materialId: id, rootPath: z.string().startsWith("/") })),
|
|
123
|
+
evaluationOutputSchemaPath: z.literal(EVAL_OUTPUT_SCHEMA_PATH).exactOptional()
|
|
60
124
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
var evalSubmitSchema = z.strictObject({
|
|
126
|
+
idempotencyKey: id,
|
|
127
|
+
prompt: z.string().trim().min(1),
|
|
128
|
+
unattended: z.literal(true),
|
|
129
|
+
runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional()
|
|
64
130
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const row = entry;
|
|
96
|
-
if (typeof row.id !== "string" || !row.id || seen.has(row.id) ||
|
|
97
|
-
!Number.isSafeInteger(row.createdAt) || Number(row.createdAt) < 0 ||
|
|
98
|
-
!row.message || typeof row.message !== "object" || Array.isArray(row.message) ||
|
|
99
|
-
!["user", "assistant", "toolResult"].includes(row.message.role ?? "")) {
|
|
100
|
-
throw new Error("Invalid or duplicate canonical context entry");
|
|
101
|
-
}
|
|
102
|
-
seen.add(row.id);
|
|
103
|
-
}
|
|
104
|
-
if (entries.length > 0 && entries[0].message.role !== "user") {
|
|
105
|
-
throw new Error("Initial context must begin with a user message");
|
|
106
|
-
}
|
|
107
|
-
return { entries: entries };
|
|
108
|
-
}
|
|
109
|
-
/** The one file an Evaluator writes its scored result to, in its own Space. */
|
|
110
|
-
export const EVAL_RESULT_FILE = "result.json";
|
|
111
|
-
export const EVAL_RESULT_PATH = `/output/${EVAL_RESULT_FILE}`;
|
|
112
|
-
export const EVAL_RESULT_MAX_BYTES = 1_000_000;
|
|
113
|
-
/** Reads the result file as raw UTF-8 text; any byte order mark and formatting are kept. */
|
|
114
|
-
export async function readEvalStructuredOutput(path, bytes) {
|
|
115
|
-
if (bytes.byteLength > EVAL_RESULT_MAX_BYTES)
|
|
116
|
-
throw new Error(`${EVAL_RESULT_FILE} exceeds ${EVAL_RESULT_MAX_BYTES} bytes`);
|
|
117
|
-
let content;
|
|
118
|
-
try {
|
|
119
|
-
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
throw new Error(`${EVAL_RESULT_FILE} is not valid UTF-8`);
|
|
131
|
+
var evalBillingResourcesSchema = z.strictObject({
|
|
132
|
+
usageEvents: z.array(z.record(z.string(), z.unknown())),
|
|
133
|
+
billingSettlements: z.array(z.record(z.string(), z.unknown()))
|
|
134
|
+
});
|
|
135
|
+
function projectEvalSubmission(input) {
|
|
136
|
+
const normalizedStatus = {
|
|
137
|
+
pending: "queued",
|
|
138
|
+
running: "running",
|
|
139
|
+
completed: "completed",
|
|
140
|
+
error: "failed",
|
|
141
|
+
skipped: "failed",
|
|
142
|
+
aborted: "interrupted"
|
|
143
|
+
}[input.status];
|
|
144
|
+
if (!normalizedStatus) throw new Error("Unknown SP Submission status");
|
|
145
|
+
return { ...input, normalizedStatus };
|
|
146
|
+
}
|
|
147
|
+
function readEvalCanonicalContext(input) {
|
|
148
|
+
if (input.content.length > EVAL_SESSION_CONTENT_MAX_CHARS) throw new Error("Initial context is too large");
|
|
149
|
+
const parsed = input.format === "json" ? JSON.parse(input.content) : { entries: input.content.split(/\r?\n/u).filter((line) => line.trim()).map((line) => JSON.parse(line)) };
|
|
150
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
|
|
151
|
+
throw new Error("Initial context must contain canonical entries");
|
|
152
|
+
}
|
|
153
|
+
const entries = parsed.entries;
|
|
154
|
+
if (entries.length > EVAL_SESSION_MAX_ENTRIES) throw new Error("Initial context has too many entries");
|
|
155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (!entry || typeof entry !== "object") throw new Error("Invalid canonical context entry");
|
|
158
|
+
const row = entry;
|
|
159
|
+
if (typeof row.id !== "string" || !row.id || seen.has(row.id) || !Number.isSafeInteger(row.createdAt) || Number(row.createdAt) < 0 || !row.message || typeof row.message !== "object" || Array.isArray(row.message) || !["user", "assistant", "toolResult"].includes(row.message.role ?? "")) {
|
|
160
|
+
throw new Error("Invalid or duplicate canonical context entry");
|
|
123
161
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
seen.add(row.id);
|
|
163
|
+
}
|
|
164
|
+
if (entries.length > 0 && entries[0].message.role !== "user") {
|
|
165
|
+
throw new Error("Initial context must begin with a user message");
|
|
166
|
+
}
|
|
167
|
+
return { entries };
|
|
127
168
|
}
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
var EVAL_RESULT_FILE = "result.json";
|
|
170
|
+
var EVAL_RESULT_PATH = `/output/${EVAL_RESULT_FILE}`;
|
|
171
|
+
var EVAL_RESULT_MAX_BYTES = 1e6;
|
|
172
|
+
async function readEvalStructuredOutput(path, bytes) {
|
|
173
|
+
if (bytes.byteLength > EVAL_RESULT_MAX_BYTES) throw new Error(`${EVAL_RESULT_FILE} exceeds ${EVAL_RESULT_MAX_BYTES} bytes`);
|
|
174
|
+
let content;
|
|
175
|
+
try {
|
|
176
|
+
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error(`${EVAL_RESULT_FILE} is not valid UTF-8`);
|
|
179
|
+
}
|
|
180
|
+
const contentHash = `sha256:${await sha256Hex(bytes)}`;
|
|
181
|
+
return { path, content, contentHash };
|
|
182
|
+
}
|
|
183
|
+
function evalMaterialRoot(bundleId, materialId) {
|
|
184
|
+
return `${EVAL_INITIAL_MATERIALS_ROOT}/${evalPathSegment(bundleId)}/${evalPathSegment(materialId)}`;
|
|
185
|
+
}
|
|
186
|
+
function evalPathSegment(value) {
|
|
187
|
+
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
130
188
|
}
|
|
131
|
-
/** Absolute paths below a root; `.`/`..`, `.git`, backslashes and NUL never reach the Space. */
|
|
132
189
|
function evalPathParts(path) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
size: z.number().int().nonnegative(),
|
|
159
|
-
mediaType: id,
|
|
190
|
+
const parts = path.split("/").slice(1);
|
|
191
|
+
if (!path.startsWith("/") || parts.length === 0 || parts.some((part) => !part || part === "." || part === ".." || part === ".git" || part.includes("\\") || part.includes("\0"))) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return parts;
|
|
195
|
+
}
|
|
196
|
+
function evalMaterialPath(root, path) {
|
|
197
|
+
const parts = evalPathParts(path);
|
|
198
|
+
if (!parts) throw new Error("Invalid initial material path");
|
|
199
|
+
return `${root}/${parts.join("/")}`;
|
|
200
|
+
}
|
|
201
|
+
var EVAL_EVIDENCE_ROOT = "/evidence";
|
|
202
|
+
var EVAL_EVIDENCE_PATHS = {
|
|
203
|
+
subjectInputs: "/subject-inputs",
|
|
204
|
+
artifacts: "/artifacts",
|
|
205
|
+
trajectory: "/trajectory",
|
|
206
|
+
manifest: "/manifest.json"
|
|
207
|
+
};
|
|
208
|
+
var EVAL_EVIDENCE_FILE_MAX_BYTES = 25e6;
|
|
209
|
+
var EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
|
|
210
|
+
var EVAL_EVIDENCE_SHA256_HEADER = "X-Eval-Content-Sha256";
|
|
211
|
+
var evalEvidenceArtifactSchema = z.strictObject({
|
|
212
|
+
path: z.string().startsWith("/").min(2),
|
|
213
|
+
size: z.number().int().nonnegative(),
|
|
214
|
+
mediaType: id
|
|
160
215
|
});
|
|
161
|
-
|
|
162
|
-
|
|
216
|
+
var evalEvidenceArtifactsSchema = z.strictObject({
|
|
217
|
+
artifacts: z.array(evalEvidenceArtifactSchema)
|
|
163
218
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
function readEvalTrajectoryPageQuery(params) {
|
|
220
|
+
const afterSeq = Number(params.get("afterSeq") ?? "0");
|
|
221
|
+
const limit = Number(params.get("limit") ?? String(EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX));
|
|
222
|
+
if (!Number.isSafeInteger(afterSeq) || afterSeq < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX) {
|
|
223
|
+
throw new Error("Invalid trajectory page");
|
|
224
|
+
}
|
|
225
|
+
return { afterSeq, limit };
|
|
226
|
+
}
|
|
227
|
+
function evalEvidencePath(relative) {
|
|
228
|
+
const parts = evalPathParts(relative);
|
|
229
|
+
if (!parts || relative !== EVAL_EVIDENCE_PATHS.manifest && ![EVAL_EVIDENCE_PATHS.subjectInputs, EVAL_EVIDENCE_PATHS.artifacts, EVAL_EVIDENCE_PATHS.trajectory].some((root) => relative.startsWith(`${root}/`))) throw new Error("Invalid evidence path");
|
|
230
|
+
return `${EVAL_EVIDENCE_ROOT}/${parts.join("/")}`;
|
|
231
|
+
}
|
|
232
|
+
function evalSubjectInputEvidencePath(bundleId, materialId, path) {
|
|
233
|
+
return evalMaterialPath(
|
|
234
|
+
`${EVAL_EVIDENCE_PATHS.subjectInputs}/${evalPathSegment(bundleId)}/${evalPathSegment(materialId)}`,
|
|
235
|
+
path
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
function evalArtifactEvidencePath(path) {
|
|
239
|
+
return evalMaterialPath(EVAL_EVIDENCE_PATHS.artifacts, path);
|
|
240
|
+
}
|
|
241
|
+
function evalTrajectoryEvidencePath(part) {
|
|
242
|
+
if (!Number.isSafeInteger(part) || part < 1) throw new Error("Invalid trajectory part");
|
|
243
|
+
return `${EVAL_EVIDENCE_PATHS.trajectory}/events-${String(part).padStart(6, "0")}.jsonl`;
|
|
244
|
+
}
|
|
245
|
+
function isEvalSha256(value) {
|
|
246
|
+
return value !== null && /^[0-9a-f]{64}$/u.test(value);
|
|
247
|
+
}
|
|
248
|
+
function stableEvalFacts(value) {
|
|
249
|
+
const facts = canonicalJsonStringify(value);
|
|
250
|
+
if (facts === void 0) throw new TypeError("Eval facts must be JSON");
|
|
251
|
+
return facts;
|
|
196
252
|
}
|
|
253
|
+
export {
|
|
254
|
+
EVAL_EVIDENCE_FILE_MAX_BYTES,
|
|
255
|
+
EVAL_EVIDENCE_PATHS,
|
|
256
|
+
EVAL_EVIDENCE_ROOT,
|
|
257
|
+
EVAL_EVIDENCE_SHA256_HEADER,
|
|
258
|
+
EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX,
|
|
259
|
+
EVAL_HARNESS_PATH,
|
|
260
|
+
EVAL_INITIALIZATION_BODY_MAX_BYTES,
|
|
261
|
+
EVAL_INITIALIZATION_MAX_BYTES,
|
|
262
|
+
EVAL_INITIAL_FILE_MAX_BYTES,
|
|
263
|
+
EVAL_INITIAL_MATERIALS_ROOT,
|
|
264
|
+
EVAL_MATERIAL_MAX_FILES,
|
|
265
|
+
EVAL_OUTPUT_SCHEMA_PATH,
|
|
266
|
+
EVAL_RESULT_FILE,
|
|
267
|
+
EVAL_RESULT_MAX_BYTES,
|
|
268
|
+
EVAL_RESULT_PATH,
|
|
269
|
+
EVAL_SERVICE_HEADER,
|
|
270
|
+
EVAL_SESSION_CONTENT_MAX_CHARS,
|
|
271
|
+
EVAL_SESSION_MAX_ENTRIES,
|
|
272
|
+
evalArtifactEvidencePath,
|
|
273
|
+
evalBillingResourcesSchema,
|
|
274
|
+
evalEnsureChatSchema,
|
|
275
|
+
evalEvidenceArtifactSchema,
|
|
276
|
+
evalEvidenceArtifactsSchema,
|
|
277
|
+
evalEvidencePath,
|
|
278
|
+
evalInitialContextSchema,
|
|
279
|
+
evalInitializationResultSchema,
|
|
280
|
+
evalInitializationSchema,
|
|
281
|
+
evalMaterialFilesByteSize,
|
|
282
|
+
evalMaterialFilesSchema,
|
|
283
|
+
evalMaterialPath,
|
|
284
|
+
evalMaterialRoot,
|
|
285
|
+
evalSubjectInputEvidencePath,
|
|
286
|
+
evalSubmitSchema,
|
|
287
|
+
evalTrajectoryEvidencePath,
|
|
288
|
+
isEvalSha256,
|
|
289
|
+
projectEvalSubmission,
|
|
290
|
+
readEvalCanonicalContext,
|
|
291
|
+
readEvalStructuredOutput,
|
|
292
|
+
readEvalTrajectoryPageQuery,
|
|
293
|
+
stableEvalFacts
|
|
294
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/eval-harness",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "SP service contract for Eval-managed Chats: routes, headers and wire schemas.",
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
6
4
|
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
7
6
|
"exports": {
|
|
8
7
|
".": {
|
|
9
8
|
"types": "./dist/index.d.ts",
|
|
@@ -14,21 +13,17 @@
|
|
|
14
13
|
"dist",
|
|
15
14
|
"README.md"
|
|
16
15
|
],
|
|
17
|
-
"
|
|
18
|
-
"access": "public",
|
|
19
|
-
"tag": "alpha"
|
|
20
|
-
},
|
|
16
|
+
"sideEffects": false,
|
|
21
17
|
"dependencies": {
|
|
22
18
|
"zod": "^4.6.5"
|
|
23
19
|
},
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"tag": "alpha"
|
|
27
23
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"build:dist": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/springbrand-lab/sp-platform.git",
|
|
27
|
+
"directory": "packages/eval-harness"
|
|
33
28
|
}
|
|
34
|
-
}
|
|
29
|
+
}
|