@springbrand/eval-harness 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # @springbrand/eval-harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SP's service contract for Eval-managed ordinary Chats. This package owns only
4
+ wire validation, stable request facts and service route names. Inbox owns Chat
5
+ creation and routing, Severn owns Submission and canonical Session history,
6
+ Workspace owns files, and API owns billing settlements. Eval Project, Dataset,
7
+ Sampling and scoring remain in `eval-platform`.
8
+
9
+ The Eval Run Worker authenticates with a dedicated service token. Its requests
10
+ include the owning user ID because an SP Inbox is addressed by user. The Chat
11
+ creation facts and each Submission operation key are immutable. Initialization
12
+ receives Eval-provided bundle/resource IDs and their content: material files go
13
+ to the target Chat Workspace; canonical JSON or JSONL context may be imported into
14
+ the Chat Session before the first Submission and need not be copied to Workspace.
15
+
16
+ The service interface is implemented under `apps/agent/src/eval/`. The Agent
17
+ Worker requires the separately provisioned `EVAL_SP_SERVICE_TOKEN` secret and
18
+ the Run Worker sends it as `X-Eval-Service-Token`. No browser Origin is accepted.
19
+ The Eval Worker uses the independently deployed Eval SP origin. Its user ID is
20
+ part of the Agent Worker path because an SP Inbox is addressed by user.
21
+ That Agent Worker's `API_ORIGIN` and SpaceDO binding must point to the same
22
+ independent Eval SP deployment; the shared SP dev API and database are not used.
23
+
24
+ | Operation | Agent Worker route |
25
+ | --- | --- |
26
+ | Ensure the ordinary Chat | `PUT /internal/v1/eval-harness/users/{userId}/chats/{chatId}` |
27
+ | Initialize one Data sample's optional Session and materials | `POST /internal/v1/eval-harness/users/{userId}/chats/{chatId}/initialization` |
28
+ | Submit one Prompt | `POST /internal/v1/eval-harness/users/{userId}/chats/{chatId}/submissions` |
29
+ | Recover a Submission by operation key | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/submissions/by-key/{key}` |
30
+ | Stop every unfinished Submission in this Eval Chat | `POST /internal/v1/eval-harness/users/{userId}/chats/{chatId}/submissions/stop` |
31
+ | Observe a Submission | `WS /internal/v1/eval-harness/users/{userId}/chats/{chatId}/submissions/{submissionId}/events` |
32
+ | Collect the complete artifact list and trajectory reference | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/output` |
33
+ | Collect raw resource facts | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/resources/by-key/{key}` |
34
+ | Export: list produced artifacts with size and media type | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/evidence/artifacts` |
35
+ | Export: read one produced file's bytes | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/evidence/files?path=` |
36
+ | Export: page the complete trajectory, oldest first | `GET /internal/v1/eval-harness/users/{userId}/chats/{chatId}/evidence/trajectory?afterSeq=&limit=` |
37
+ | Import: write one evidence file into an Evaluator's own Space | `PUT /internal/v1/eval-harness/users/{userId}/chats/{chatId}/evidence/files?path=` |
38
+
39
+ The initialization body includes the Data sample and, only when referenced by
40
+ that sample, `initialSession: {bundleId, sessionId, context?: {format, content}}`
41
+ and/or `initialMaterials: [{bundleId, materialId, files}]`. `content` is canonical
42
+ Session JSON or JSONL; each material file supplies its path, media type and
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.
65
+
66
+ An Evaluator is an ordinary Eval Chat with its own Session, Submission and
67
+ Space; its creation facts add only `evaluationId`. It never reads or writes the
68
+ Subject's Space. Eval Platform exports a finished Subject's evidence through the
69
+ read-only export routes and freezes it as a content-addressed bundle, then imports
70
+ that bundle into the Evaluator's own Space under `/evidence` before the first
71
+ Submission. Export routes answer only after the Chat's last Submission ended and
72
+ its whole trajectory was delivered (409 before that). The artifact list covers
73
+ files the Agent produced in its own Space, excluding `/reference`,
74
+ `/initial-materials`, `/userspace`, `/evidence` and uploads; a single file crosses
75
+ one Durable Object RPC, so files over 25 MB are refused with 413. Trajectory pages
76
+ come straight from `trajectory_events` with a forward `afterSeq` cursor
77
+ (`nextAfterSeq` is null after the last page). An import `PUT` carries the raw
78
+ bytes, their `Content-Type` and `X-Eval-Content-Sha256: <hex>`; SP verifies the
79
+ hash, writes `/evidence/<path>` and returns `{path, size, sha256}`. Only an
80
+ Evaluator imports; an identical repeat is a no-op, different content is a 409,
81
+ and imports stop with the Chat's stop latch.
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
+
123
+ Eval Platform writes the Evaluator instructions into the Prompt; SP submits it
124
+ unchanged. The Evaluator's `output` adds `structuredOutput: {path, content,
125
+ contentHash}` with the raw UTF-8 text of `/output/result.json` in its own Space
126
+ (at most 1 MB, `contentHash` is `sha256:<hex>`); it is omitted when the Evaluator
127
+ wrote no result file.
128
+ The Chat observer WebSocket carries status only. Browser Chat input and writes
129
+ are rejected. SP API provides its account, Agent Config, and billing facts.
130
+ The service-only stop route delegates to the ordinary Chat Submission lifecycle:
131
+ it persists an Eval Chat stop latch, waits for already accepted submit calls,
132
+ interrupts active work, and returns after terminal status is recorded. New
133
+ initialization and Submission requests are rejected after the latch is saved,
134
+ including after a DO restart.
135
+ It refuses a Chat without an Eval binding. Repeating stop after all Submissions
136
+ finish returns `{ "ok": false }` without changing history.
@@ -0,0 +1,234 @@
1
+ import { z } from "zod";
2
+ export declare const EVAL_SERVICE_HEADER = "X-Eval-Service-Token";
3
+ export declare const EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
4
+ export declare const evalEnsureChatSchema: z.ZodObject<{
5
+ chatId: z.ZodUUID;
6
+ role: z.ZodEnum<{
7
+ subject: "subject";
8
+ evaluator: "evaluator";
9
+ }>;
10
+ framework: z.ZodLiteral<"sp">;
11
+ samplingId: z.ZodString;
12
+ projectId: z.ZodString;
13
+ agentConfig: z.ZodObject<{
14
+ ownerUserId: z.ZodUUID;
15
+ userAgentId: z.ZodUUID;
16
+ }, z.core.$strict>;
17
+ evaluationId: z.ZodOptional<z.ZodString>;
18
+ runtime: z.ZodOptional<z.ZodObject<{
19
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
20
+ }, z.core.$strict>>;
21
+ }, z.core.$strict>;
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<{
36
+ path: z.ZodString;
37
+ mediaType: z.ZodString;
38
+ contentBase64: z.ZodBase64;
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>>;
45
+ export declare const evalInitialContextSchema: z.ZodObject<{
46
+ format: z.ZodEnum<{
47
+ json: "json";
48
+ jsonl: "jsonl";
49
+ }>;
50
+ content: z.ZodString;
51
+ }, z.core.$strict>;
52
+ export declare const evalInitializationSchema: z.ZodObject<{
53
+ idempotencyKey: z.ZodString;
54
+ data: z.ZodIntersection<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodObject<{
55
+ userPrompt: z.ZodString;
56
+ }, z.core.$strip>>;
57
+ initialSession: z.ZodOptional<z.ZodObject<{
58
+ bundleId: z.ZodString;
59
+ sessionId: z.ZodString;
60
+ context: z.ZodOptional<z.ZodObject<{
61
+ format: z.ZodEnum<{
62
+ json: "json";
63
+ jsonl: "jsonl";
64
+ }>;
65
+ content: z.ZodString;
66
+ }, z.core.$strict>>;
67
+ }, z.core.$strict>>;
68
+ initialMaterials: z.ZodOptional<z.ZodArray<z.ZodObject<{
69
+ bundleId: z.ZodString;
70
+ materialId: z.ZodString;
71
+ files: z.ZodArray<z.ZodObject<{
72
+ path: z.ZodString;
73
+ mediaType: z.ZodString;
74
+ contentBase64: z.ZodBase64;
75
+ }, z.core.$strict>>;
76
+ }, z.core.$strict>>>;
77
+ evaluationOutputSchema: z.ZodOptional<z.ZodObject<{
78
+ content: z.ZodString;
79
+ }, z.core.$strict>>;
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>;
91
+ export declare const evalSubmitSchema: z.ZodObject<{
92
+ idempotencyKey: z.ZodString;
93
+ prompt: z.ZodString;
94
+ unattended: z.ZodLiteral<true>;
95
+ runtime: z.ZodOptional<z.ZodObject<{
96
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
97
+ }, z.core.$strict>>;
98
+ }, z.core.$strict>;
99
+ export type EvalEnsureChat = z.output<typeof evalEnsureChatSchema>;
100
+ export type EvalMaterialFile = z.output<typeof evalMaterialFileSchema>;
101
+ export type EvalInitialization = z.output<typeof evalInitializationSchema>;
102
+ export type EvalInitializationResult = z.output<typeof evalInitializationResultSchema>;
103
+ export type EvalSubmit = z.output<typeof evalSubmitSchema>;
104
+ export type EvalSubmissionReceipt = {
105
+ readonly submissionId: string;
106
+ readonly status: string;
107
+ readonly normalizedStatus: "queued" | "running" | "completed" | "failed" | "interrupted" | "cancelled";
108
+ readonly accepted: boolean;
109
+ readonly error?: string;
110
+ readonly retryable?: boolean;
111
+ readonly createdAt: number;
112
+ readonly completedAt?: number;
113
+ };
114
+ export declare const evalBillingResourcesSchema: z.ZodObject<{
115
+ usageEvents: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
116
+ billingSettlements: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
117
+ }, z.core.$strict>;
118
+ export type EvalBillingResources = z.output<typeof evalBillingResourcesSchema>;
119
+ export declare function projectEvalSubmission(input: {
120
+ readonly submissionId: string;
121
+ readonly status: string;
122
+ readonly accepted: boolean;
123
+ readonly error?: string;
124
+ readonly retryable?: boolean;
125
+ readonly createdAt: number;
126
+ readonly completedAt?: number;
127
+ }): EvalSubmissionReceipt;
128
+ export type EvalCanonicalContext = {
129
+ readonly entries: readonly {
130
+ readonly id: string;
131
+ readonly message: Readonly<Record<string, unknown>>;
132
+ readonly createdAt: number;
133
+ }[];
134
+ };
135
+ /** JSON stores one canonical snapshot; JSONL stores one canonical entry per line. */
136
+ export declare function readEvalCanonicalContext(input: z.output<typeof evalInitialContextSchema>): EvalCanonicalContext;
137
+ /** The one file an Evaluator writes its scored result to, in its own Space. */
138
+ export declare const EVAL_RESULT_FILE = "result.json";
139
+ export declare const EVAL_RESULT_PATH = "/output/result.json";
140
+ export declare const EVAL_RESULT_MAX_BYTES = 1000000;
141
+ /** The Evaluator result file exactly as written; Eval Platform parses and validates it. */
142
+ export type EvalStructuredOutput = {
143
+ readonly path: string;
144
+ readonly content: string;
145
+ readonly contentHash: string;
146
+ };
147
+ /** Reads the result file as raw UTF-8 text; any byte order mark and formatting are kept. */
148
+ export declare function readEvalStructuredOutput(path: string, bytes: Uint8Array): Promise<EvalStructuredOutput>;
149
+ /** The `output` route body; an Evaluator's `structuredOutput` is omitted when it wrote no result file. */
150
+ export type EvalEvidence = {
151
+ readonly status: "succeeded" | "failed";
152
+ readonly samplingId: string;
153
+ readonly evaluationId?: string;
154
+ readonly artifacts: readonly {
155
+ readonly artifactId: string;
156
+ readonly kind: string;
157
+ readonly path: string;
158
+ }[];
159
+ readonly trajectory: {
160
+ readonly trajectoryId: string;
161
+ readonly source: "subject" | "evaluator";
162
+ readonly path: string;
163
+ };
164
+ readonly structuredOutput?: EvalStructuredOutput;
165
+ readonly error?: string;
166
+ };
167
+ export declare function evalMaterialRoot(bundleId: string, materialId: string): string;
168
+ export declare function evalMaterialPath(root: string, path: string): string;
169
+ /** Where imported evidence lives in the Evaluator's own Space. */
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
+ };
178
+ /** One exported or imported file crosses a single Durable Object RPC as bytes. */
179
+ export declare const EVAL_EVIDENCE_FILE_MAX_BYTES = 25000000;
180
+ export declare const EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
181
+ export declare const EVAL_EVIDENCE_SHA256_HEADER = "X-Eval-Content-Sha256";
182
+ /** One Subject artifact; its bytes are read separately so the inventory stays small. */
183
+ export declare const evalEvidenceArtifactSchema: z.ZodObject<{
184
+ path: z.ZodString;
185
+ size: z.ZodNumber;
186
+ mediaType: z.ZodString;
187
+ }, z.core.$strict>;
188
+ export type EvalEvidenceArtifact = z.output<typeof evalEvidenceArtifactSchema>;
189
+ export declare const evalEvidenceArtifactsSchema: z.ZodObject<{
190
+ artifacts: z.ZodArray<z.ZodObject<{
191
+ path: z.ZodString;
192
+ size: z.ZodNumber;
193
+ mediaType: z.ZodString;
194
+ }, z.core.$strict>>;
195
+ }, z.core.$strict>;
196
+ export type EvalEvidenceArtifacts = z.output<typeof evalEvidenceArtifactsSchema>;
197
+ /** Forward pages of the Chat's complete `trajectory_events`, oldest first. */
198
+ export type EvalEvidenceTrajectoryPage = {
199
+ readonly events: readonly {
200
+ readonly ingestSeq: number;
201
+ readonly event: object;
202
+ }[];
203
+ /** Pass as `afterSeq` for the next page; null once the last event was returned. */
204
+ readonly nextAfterSeq: number | null;
205
+ };
206
+ export declare function readEvalTrajectoryPageQuery(params: URLSearchParams): {
207
+ afterSeq: number;
208
+ limit: number;
209
+ };
210
+ /** A Space path for one evidence file; `relative` keeps the bundle's own layout below `/evidence`. */
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
+ };
225
+ /** The receipt for one imported file; identical repeats return the same receipt. */
226
+ export type EvalEvidenceFileReceipt = {
227
+ readonly path: string;
228
+ readonly size: number;
229
+ readonly sha256: string;
230
+ };
231
+ export declare function isEvalSha256(value: string | null): value is string;
232
+ /** Compare creation and operation facts without depending on JSON property order. */
233
+ export declare function stableEvalFacts(value: unknown): string;
234
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,294 @@
1
+ // packages/eval-harness/src/index.ts
2
+ import { z } from "zod";
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()
36
+ }).superRefine((value, context) => {
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))
63
+ });
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
+ });
70
+ });
71
+ var evalInitialMaterialSchema = z.strictObject({
72
+ bundleId: id,
73
+ materialId: id,
74
+ files: evalMaterialFilesSchema
75
+ });
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()
92
+ }).superRefine((value, context) => {
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
+ }
118
+ });
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()
124
+ });
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()
130
+ });
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");
161
+ }
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 };
168
+ }
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");
188
+ }
189
+ function evalPathParts(path) {
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
215
+ });
216
+ var evalEvidenceArtifactsSchema = z.strictObject({
217
+ artifacts: z.array(evalEvidenceArtifactSchema)
218
+ });
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;
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,6 +1,29 @@
1
1
  {
2
2
  "name": "@springbrand/eval-harness",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.1",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "sideEffects": false,
17
+ "dependencies": {
18
+ "zod": "^4.6.5"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "tag": "alpha"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/springbrand-lab/sp-platform.git",
27
+ "directory": "packages/eval-harness"
28
+ }
29
+ }