@leostera/evalkit 0.0.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.
@@ -0,0 +1,995 @@
1
+ import { A as TrialManifestSchema, D as RunSummarySchema, F as canonicalParameters, H as resourceUri, M as TrialScoringSchema, N as TrialSummarySchema, O as ScoreResultSchema, T as RunMetadataSchema, b as CheckpointResultSchema, g as parseTrajectoryJsonl, j as TrialMetadataSchema, k as TrajectoryEventSchema, n as authoringId, p as recordError, w as RunManifestSchema, x as JsonValueSchema } from "./src-CopGdrwX.mjs";
2
+ import * as Schema from "effect/Schema";
3
+ import { Data, Effect, Either, Exit, Scope } from "effect";
4
+ import { appendFile, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
5
+ import path, { join } from "node:path";
6
+ import os from "node:os";
7
+ import { randomUUID } from "node:crypto";
8
+ //#region ../runner/src/errors.ts
9
+ var AutExecutionError = class extends Data.TaggedError("AutExecutionError") {};
10
+ var FixtureError = class extends Data.TaggedError("FixtureError") {};
11
+ var ReportError = class extends Data.TaggedError("ReportError") {};
12
+ var ScoringError = class extends Data.TaggedError("ScoringError") {};
13
+ var RuleExecutionError = class extends Data.TaggedError("RuleExecutionError") {};
14
+ var CheckpointExecutionError = class extends Data.TaggedError("CheckpointExecutionError") {};
15
+ //#endregion
16
+ //#region ../runner/src/snapshot.ts
17
+ /**
18
+ * Copies every regular file in the candidate workspace into the trial report.
19
+ * Symbolic links are rejected so snapshots cannot follow paths outside the trial.
20
+ */
21
+ async function snapshotCandidateWorkspace(candidateRoot, writer) {
22
+ const artifacts = [];
23
+ async function visit(directory, relativeDirectory) {
24
+ const entries = await readdir(directory, { withFileTypes: true });
25
+ entries.sort((left, right) => left.name.localeCompare(right.name));
26
+ for (const entry of entries) {
27
+ const absolutePath = path.join(directory, entry.name);
28
+ const relativePath = path.join(relativeDirectory, entry.name);
29
+ const stat = await lstat(absolutePath);
30
+ if (stat.isSymbolicLink()) throw new Error(`Candidate workspace contains a symbolic link: ${relativePath}`);
31
+ if (stat.isDirectory()) {
32
+ artifacts.push({
33
+ path: path.join("candidate", relativePath),
34
+ kind: "directory"
35
+ });
36
+ await visit(absolutePath, relativePath);
37
+ continue;
38
+ }
39
+ if (!stat.isFile()) throw new Error(`Candidate workspace contains an unsupported entry: ${relativePath}`);
40
+ const data = await readFile(absolutePath);
41
+ const artifactPath = path.join("candidate", relativePath);
42
+ await writer.writeArtifact(artifactPath, data);
43
+ artifacts.push({
44
+ path: artifactPath,
45
+ kind: "file",
46
+ size: data.byteLength
47
+ });
48
+ }
49
+ }
50
+ await visit(candidateRoot, "");
51
+ return artifacts;
52
+ }
53
+ //#endregion
54
+ //#region ../runner/src/workspace.ts
55
+ const MAX_DYNAMIC_FIXTURE_DEPTH = 16;
56
+ function workspaceRoot(visibility, artifacts) {
57
+ return visibility === "candidate" ? artifacts.candidate.root : artifacts.evaluator.root;
58
+ }
59
+ function destinationPath(root, destination) {
60
+ if (!destination || path.isAbsolute(destination)) throw new Error(`Fixture destination must be a non-empty relative path: ${destination}`);
61
+ const normalized = path.normalize(destination);
62
+ if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Fixture destination escapes its workspace: ${destination}`);
63
+ return path.join(root, normalized);
64
+ }
65
+ async function resolveFixtures(fixtures, context, depth = 0) {
66
+ if (depth > MAX_DYNAMIC_FIXTURE_DEPTH) throw new Error("Dynamic fixture nesting exceeds the supported depth");
67
+ const resolved = [];
68
+ for (const fixture of fixtures) {
69
+ if (fixture.kind !== "dynamic") {
70
+ resolved.push(fixture);
71
+ continue;
72
+ }
73
+ const created = await fixture.create(context);
74
+ const nested = Array.isArray(created) ? created : [created];
75
+ resolved.push(...await resolveFixtures(nested, context, depth + 1));
76
+ }
77
+ return resolved;
78
+ }
79
+ function assertUniqueDestinations(fixtures) {
80
+ const destinations = /* @__PURE__ */ new Set();
81
+ for (const fixture of fixtures) {
82
+ const destination = fixture.kind === "inline" ? fixture.file : fixture.dst;
83
+ const key = `${fixture.visibility}:${destination}`;
84
+ if (destinations.has(key)) throw new Error(`Duplicate ${fixture.visibility} fixture destination: ${destination}`);
85
+ destinations.add(key);
86
+ }
87
+ }
88
+ /** Creates isolated candidate and evaluator directories and materializes fixtures into each. */
89
+ function trialDirectoryName(trialId) {
90
+ return trialId.replace(/[^a-zA-Z0-9._-]/g, "_");
91
+ }
92
+ async function createTrialWorkspace(context, fixtures = [], persistentRoot) {
93
+ const root = persistentRoot ? path.join(path.resolve(persistentRoot), trialDirectoryName(context.trialId)) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
94
+ const persistent = persistentRoot !== void 0;
95
+ const artifacts = {
96
+ candidate: { root: path.join(root, "candidate") },
97
+ evaluator: { root: path.join(root, "evaluator") }
98
+ };
99
+ try {
100
+ await Promise.all([mkdir(artifacts.candidate.root, { recursive: true }), mkdir(artifacts.evaluator.root, { recursive: true })]);
101
+ const resolved = await resolveFixtures(fixtures, context);
102
+ assertUniqueDestinations(resolved);
103
+ for (const fixture of resolved) {
104
+ const target = destinationPath(workspaceRoot(fixture.visibility, artifacts), fixture.kind === "inline" ? fixture.file : fixture.dst);
105
+ await mkdir(path.dirname(target), { recursive: true });
106
+ if (fixture.kind === "directory") await cp(fixture.src, target, {
107
+ recursive: true,
108
+ force: false,
109
+ errorOnExist: true
110
+ });
111
+ else if (fixture.kind === "file") await copyFile(fixture.src, target);
112
+ else await writeFile(target, fixture.data);
113
+ }
114
+ return {
115
+ artifacts,
116
+ cleanup: persistent ? async () => {} : () => rm(root, {
117
+ recursive: true,
118
+ force: true
119
+ })
120
+ };
121
+ } catch (error) {
122
+ await rm(root, {
123
+ recursive: true,
124
+ force: true
125
+ });
126
+ throw error;
127
+ }
128
+ }
129
+ //#endregion
130
+ //#region ../runner/src/score.ts
131
+ /** Shared normalization for inline and final predicate and judge rules. */
132
+ function normalizeScore(value) {
133
+ const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
134
+ if (!normalized || !Number.isFinite(normalized.value) || normalized.value < 0 || normalized.value > 1) throw new Error(`Score value must be a finite number from 0 to 1; received ${normalized?.value}`);
135
+ if ("judge" in normalized && normalized.judge) {
136
+ const { usage } = normalized.judge;
137
+ if (usage && Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
138
+ }
139
+ return {
140
+ value: normalized.value,
141
+ passed: normalized.passed ?? normalized.value === 1,
142
+ ...normalized.explanation ? { explanation: normalized.explanation } : {},
143
+ ...normalized.evidence === void 0 ? {} : { evidence: normalized.evidence },
144
+ ..."judge" in normalized && normalized.judge !== void 0 ? { judge: normalized.judge } : {}
145
+ };
146
+ }
147
+ //#endregion
148
+ //#region ../runner/src/judge-agent.ts
149
+ const failure$2 = (name, cause) => new RuleExecutionError({
150
+ name,
151
+ cause,
152
+ message: cause instanceof Error ? cause.message : String(cause)
153
+ });
154
+ /** Drive a distinct agent session for one assessment; do not add its events to AUT trajectory. */
155
+ function runJudgeAgent(rule, agent, input, placement) {
156
+ const events = [];
157
+ const context = {
158
+ ...input.context,
159
+ workspace: input.artifacts.evaluator,
160
+ evaluatorWorkspace: input.artifacts.evaluator
161
+ };
162
+ const prompt = JSON.stringify({
163
+ name: rule.name,
164
+ rubric: rule.rubric,
165
+ placement,
166
+ evidence: placement === "transcript" ? { turn: input.turn?.events ?? [] } : { trajectory: input.trajectory.events },
167
+ instruction: "Return only a JSON verdict with value (a number from 0 to 1), optional passed, explanation, and evidence."
168
+ });
169
+ let closeError;
170
+ return Effect.gen(function* () {
171
+ yield* Effect.acquireUseRelease(Effect.tryPromise({
172
+ try: () => agent.start({
173
+ context,
174
+ onEvent: (event) => {
175
+ const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
176
+ ...event,
177
+ source: "aut"
178
+ });
179
+ if (observed.source !== "aut") throw new Error("Invalid judge event source");
180
+ const { source: _, ...validated } = observed;
181
+ events.push(validated);
182
+ }
183
+ }),
184
+ catch: (cause) => failure$2(rule.name, cause)
185
+ }), (session) => Effect.tryPromise({
186
+ try: () => session.send(prompt),
187
+ catch: (cause) => failure$2(rule.name, cause)
188
+ }), (session) => Effect.tryPromise({
189
+ try: () => session.close(),
190
+ catch: (cause) => failure$2(rule.name, cause)
191
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
192
+ closeError = error;
193
+ }))));
194
+ if (closeError) return yield* Effect.fail(closeError);
195
+ const completed = events.slice().reverse().find((event) => event.kind === "completed" && event.output !== void 0);
196
+ const assistant = events.slice().reverse().find((event) => event.kind === "message" && event.role === "assistant");
197
+ const output = completed?.kind === "completed" ? completed.output : assistant?.kind === "message" ? assistant.content : void 0;
198
+ if (output === void 0) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge agent did not emit a verdict")));
199
+ const value = yield* Effect.try({
200
+ try: () => typeof output === "string" ? JSON.parse(output) : output,
201
+ catch: (cause) => failure$2(rule.name, cause)
202
+ });
203
+ const encodedEvents = yield* Schema.decodeUnknown(JsonValueSchema)(events).pipe(Effect.mapError((cause) => failure$2(rule.name, cause)));
204
+ if (!Array.isArray(encodedEvents)) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge events must be an array")));
205
+ const usage = events.slice().reverse().find((event) => event.kind === "turn-completed" && event.usage);
206
+ return {
207
+ value,
208
+ judge: {
209
+ ...agent.identity ? { agent: agent.identity } : {},
210
+ ...usage?.kind === "turn-completed" && usage.usage ? { usage: usage.usage } : {},
211
+ events: encodedEvents
212
+ }
213
+ };
214
+ });
215
+ }
216
+ //#endregion
217
+ //#region ../runner/src/evaluate-rule.ts
218
+ const failure$1 = (name, cause) => new RuleExecutionError({
219
+ name,
220
+ cause,
221
+ message: cause instanceof Error ? cause.message : String(cause)
222
+ });
223
+ /** Both placements call the same rule executor; only input timing and persistence differ. */
224
+ function evaluateRule(rule, input, judgeAgent, placement) {
225
+ return Effect.gen(function* () {
226
+ if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("Judge rules require an eval judge agent")));
227
+ const evaluated = rule.kind === "predicate" ? { value: yield* Effect.tryPromise({
228
+ try: async () => rule.run(input),
229
+ catch: (cause) => failure$1(rule.name, cause)
230
+ }) } : yield* runJudgeAgent(rule, judgeAgent, input, placement);
231
+ const score = yield* Effect.try({
232
+ try: () => normalizeScore(evaluated.value),
233
+ catch: (cause) => failure$1(rule.name, cause)
234
+ });
235
+ const withJudge = rule.kind === "judge" && "judge" in evaluated ? {
236
+ ...score,
237
+ judge: evaluated.judge
238
+ } : score;
239
+ const validated = yield* Schema.decodeUnknown(ScoreResultSchema)({
240
+ name: rule.name,
241
+ kind: rule.kind,
242
+ durationMs: 0,
243
+ ...withJudge
244
+ }).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
245
+ return {
246
+ value: validated.value,
247
+ passed: validated.passed,
248
+ ...validated.explanation === void 0 ? {} : { explanation: validated.explanation },
249
+ ...validated.evidence === void 0 ? {} : { evidence: validated.evidence },
250
+ ...validated.judge === void 0 ? {} : { judge: validated.judge }
251
+ };
252
+ });
253
+ }
254
+ //#endregion
255
+ //#region ../runner/src/turn.ts
256
+ /** Freeze the observation window at send() completion, before checkpoint events arrive. */
257
+ function turnView(events, start, end, userStepIndex) {
258
+ const window = events.slice(start, end);
259
+ const autEvents = window.filter((event) => event.source === "aut");
260
+ const assistantMessages = autEvents.filter((event) => event.kind === "message" && event.role === "assistant");
261
+ const calls = window.flatMap((event, index) => event.source === "aut" && event.kind === "tool-call" ? [{
262
+ event,
263
+ eventIndex: start + index
264
+ }] : []);
265
+ const results = autEvents.filter((event) => event.kind === "tool-result");
266
+ return {
267
+ userStepIndex,
268
+ events: autEvents,
269
+ assistantMessages,
270
+ toolCalls: calls.map(({ event, eventIndex }) => {
271
+ const matches = results.filter((result) => result.id === event.id);
272
+ const ambiguous = calls.filter((call) => call.event.id === event.id).length !== 1 || matches.length > 1;
273
+ return {
274
+ eventIndex,
275
+ id: event.id,
276
+ name: event.name,
277
+ arguments: event.arguments,
278
+ resultObservation: ambiguous ? "ambiguous" : matches.length ? "observed" : "absent",
279
+ ...!ambiguous && matches.length ? { result: matches[0].result } : {}
280
+ };
281
+ }),
282
+ ...typeof assistantMessages.at(-1)?.content === "string" ? { lastAssistantText: assistantMessages.at(-1).content } : {}
283
+ };
284
+ }
285
+ /** Exact JSON matching: object keys do not matter, array order does. */
286
+ function matchingToolCall(turn, name, args) {
287
+ const expected = args === void 0 ? void 0 : canonicalParameters(args);
288
+ return turn.toolCalls.find((call) => call.name === name && (expected === void 0 || canonicalParameters(call.arguments) === expected));
289
+ }
290
+ //#endregion
291
+ //#region ../runner/src/checkpoint.ts
292
+ const failure = (step, name, cause) => new CheckpointExecutionError({
293
+ step,
294
+ name,
295
+ cause,
296
+ message: cause instanceof Error ? cause.message : String(cause)
297
+ });
298
+ /** An assertion failure is a successful Effect containing a failed result; only execution errors enter the error channel. */
299
+ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
300
+ const annotations = {
301
+ evalId: input.context.evalId,
302
+ runId: input.context.runId,
303
+ trialId: input.context.trialId,
304
+ step: index,
305
+ checkpoint: step.name
306
+ };
307
+ return Effect.gen(function* () {
308
+ const started = now();
309
+ yield* Effect.logDebug("checkpoint started");
310
+ const match = step.kind === "expect-tool-call" ? yield* Effect.try({
311
+ try: () => matchingToolCall(input.turn, step.expected.name, step.expected.arguments),
312
+ catch: (cause) => failure(index, step.name, cause)
313
+ }) : void 0;
314
+ const score = step.kind === "expect-tool-call" ? yield* Effect.try({
315
+ try: () => normalizeScore(Boolean(match)),
316
+ catch: (cause) => failure(index, step.name, cause)
317
+ }) : yield* evaluateRule(step, input, judgeAgent, "transcript").pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
318
+ const result = yield* Schema.decodeUnknown(CheckpointResultSchema)({
319
+ step: index,
320
+ kind: step.kind,
321
+ name: step.name,
322
+ status: score.passed ? "passed" : "failed",
323
+ durationMs: now().getTime() - started.getTime(),
324
+ ...score,
325
+ ...match ? { matchedToolCall: {
326
+ eventIndex: match.eventIndex,
327
+ id: match.id
328
+ } } : {}
329
+ }).pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
330
+ yield* Effect.logInfo("checkpoint completed").pipe(Effect.annotateLogs({ status: result.status }));
331
+ return {
332
+ ...result,
333
+ status: score.passed ? "passed" : "failed"
334
+ };
335
+ }).pipe(Effect.tapError((error) => Effect.logError("checkpoint execution error").pipe(Effect.annotateLogs({ errorName: error._tag }))), Effect.annotateLogs(annotations));
336
+ }
337
+ //#endregion
338
+ //#region ../runner/src/effect-boundary.ts
339
+ /** Keep tagged Effect failures intact when bridging into the legacy async reporting lifecycle. Defects still reject. */
340
+ async function runBoundary(effect) {
341
+ const outcome = await Effect.runPromise(Effect.either(effect));
342
+ if (Either.isLeft(outcome)) throw outcome.left;
343
+ return outcome.right;
344
+ }
345
+ //#endregion
346
+ //#region ../runner/src/transcript.ts
347
+ async function executeTranscript(definition, session, input) {
348
+ const { now, emitRunner, progress } = input;
349
+ for (const [index, step] of definition.transcript.entries()) {
350
+ await emitRunner({
351
+ kind: "transcript-step-started",
352
+ step: index,
353
+ timestamp: now().toISOString()
354
+ });
355
+ if (step.kind === "user") {
356
+ const cursor = input.events.length;
357
+ await sendUserTurn(session, step.message.replaceAll("{{randomSeed}}", String(input.randomSeed)));
358
+ progress.lastTurn = turnView(input.events, cursor, input.events.length, index);
359
+ } else if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") {
360
+ const result = await runCheckpoint(definition, step, index, input);
361
+ progress.checkpoints.push(result);
362
+ await emitRunner({
363
+ kind: "checkpoint-completed",
364
+ step: index,
365
+ status: result.status,
366
+ timestamp: now().toISOString()
367
+ });
368
+ await emitRunner({
369
+ kind: "transcript-step-completed",
370
+ step: index,
371
+ timestamp: now().toISOString()
372
+ });
373
+ if (!result.passed && definition.policy?.failfast) {
374
+ progress.stoppedEarly = true;
375
+ await skipRemainingSteps(definition, index, input);
376
+ break;
377
+ }
378
+ continue;
379
+ } else throw new Error(`Transcript step kind "${step.kind}" is not executable yet`);
380
+ await emitRunner({
381
+ kind: "transcript-step-completed",
382
+ step: index,
383
+ timestamp: now().toISOString()
384
+ });
385
+ }
386
+ }
387
+ async function sendUserTurn(session, message) {
388
+ await runBoundary(Effect.tryPromise({
389
+ try: () => session.send(message),
390
+ catch: (cause) => cause instanceof ReportError ? cause : new AutExecutionError({
391
+ cause,
392
+ message: cause instanceof Error ? cause.message : String(cause)
393
+ })
394
+ }));
395
+ }
396
+ async function runCheckpoint(definition, step, index, input) {
397
+ const { now, emitRunner, progress } = input;
398
+ if (!progress.lastTurn) throw new Error("Checkpoint requires a preceding user step");
399
+ const started = now();
400
+ await emitRunner({
401
+ kind: "checkpoint-started",
402
+ step: index,
403
+ name: step.name,
404
+ timestamp: started.toISOString()
405
+ });
406
+ return runBoundary(evaluateCheckpoint(step, index, {
407
+ context: input.context,
408
+ artifacts: input.artifacts,
409
+ trajectory: { events: input.events },
410
+ turn: progress.lastTurn
411
+ }, now, definition.judge)).catch(async (error) => {
412
+ const recorded = recordError(error);
413
+ progress.checkpoints.push({
414
+ step: index,
415
+ kind: step.kind,
416
+ name: step.name,
417
+ status: "error",
418
+ durationMs: now().getTime() - started.getTime(),
419
+ error: recorded
420
+ });
421
+ try {
422
+ await emitRunner({
423
+ kind: "checkpoint-error",
424
+ step: index,
425
+ error: recorded,
426
+ timestamp: now().toISOString()
427
+ });
428
+ } catch {}
429
+ throw error;
430
+ });
431
+ }
432
+ async function skipRemainingSteps(definition, after, { now, emitRunner, progress }) {
433
+ for (let index = after + 1; index < definition.transcript.length; index++) {
434
+ await emitRunner({
435
+ kind: "transcript-step-skipped",
436
+ step: index,
437
+ timestamp: now().toISOString()
438
+ });
439
+ const step = definition.transcript[index];
440
+ if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") progress.checkpoints.push({
441
+ step: index,
442
+ kind: step.kind,
443
+ name: step.name,
444
+ status: "skipped"
445
+ });
446
+ }
447
+ }
448
+ //#endregion
449
+ //#region ../runner/src/final-scoring.ts
450
+ function scoreSummary(results, checkpoints, skippedScorers) {
451
+ const valid = results.filter((result) => result.value !== void 0);
452
+ const hasError = results.some((result) => result.error !== void 0);
453
+ return {
454
+ results,
455
+ ...checkpoints.length ? { checkpoints } : {},
456
+ ...skippedScorers.length ? { skippedScorers } : {},
457
+ ...valid.length === 0 ? {} : { overall: valid.reduce((sum, result) => sum + (result.value ?? 0), 0) / valid.length },
458
+ passed: !hasError && results.every((result) => result.passed === true) && checkpoints.every((result) => result.status === "passed") && !skippedScorers.length
459
+ };
460
+ }
461
+ /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
462
+ async function evaluateFinalScorers(definition, input) {
463
+ const { context, workspace, events, lastTurn, results, skippedScorers, now, emitRunner } = input;
464
+ for (const scorer of definition.scoring) {
465
+ if (input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
466
+ skippedScorers.push(scorer.name);
467
+ continue;
468
+ }
469
+ const started = now();
470
+ await emitRunner({
471
+ kind: "scorer-started",
472
+ scorer: scorer.name,
473
+ timestamp: started.toISOString()
474
+ });
475
+ let result;
476
+ try {
477
+ const score = await runBoundary(evaluateRule(scorer, {
478
+ context,
479
+ artifacts: workspace.artifacts,
480
+ trajectory: { events },
481
+ ...lastTurn ? { turn: lastTurn } : {}
482
+ }, definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
483
+ cause,
484
+ message: cause.message
485
+ }))));
486
+ result = {
487
+ name: scorer.name,
488
+ kind: scorer.kind,
489
+ durationMs: now().getTime() - started.getTime(),
490
+ ...score
491
+ };
492
+ results.push(result);
493
+ } catch (error) {
494
+ const recorded = recordError(error);
495
+ results.push({
496
+ name: scorer.name,
497
+ kind: scorer.kind,
498
+ durationMs: now().getTime() - started.getTime(),
499
+ error: recorded,
500
+ passed: false
501
+ });
502
+ await emitRunner({
503
+ kind: "scorer-failed",
504
+ scorer: scorer.name,
505
+ error: recorded,
506
+ timestamp: now().toISOString()
507
+ });
508
+ continue;
509
+ }
510
+ await emitRunner({
511
+ kind: "scorer-completed",
512
+ scorer: scorer.name,
513
+ value: result.value ?? 0,
514
+ timestamp: now().toISOString()
515
+ });
516
+ }
517
+ }
518
+ //#endregion
519
+ //#region ../runner/src/local-report-store.ts
520
+ const MANIFEST_FILE = "manifest.json";
521
+ async function writeJson(filePath, value) {
522
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
523
+ }
524
+ async function replaceJson(filePath, value) {
525
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
526
+ await writeJson(temporary, value);
527
+ await rename(temporary, filePath);
528
+ }
529
+ function assertPathSegment(value, name) {
530
+ if (!value || value === "." || value === ".." || value.includes("/") || value.includes("\\")) throw new Error(`${name} must be a single relative path segment`);
531
+ }
532
+ function artifactPath(root, value) {
533
+ if (!value || path.isAbsolute(value)) throw new Error(`Artifact path must be a non-empty relative path: ${value}`);
534
+ const normalized = path.normalize(value);
535
+ if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
536
+ return path.join(root, normalized);
537
+ }
538
+ var LocalTrialWriter = class {
539
+ directory;
540
+ metadata;
541
+ constructor(directory, metadata) {
542
+ this.directory = directory;
543
+ this.metadata = metadata;
544
+ }
545
+ appendEvent(event) {
546
+ return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
547
+ }
548
+ writeScores(scoring) {
549
+ return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(TrialScoringSchema)(scoring));
550
+ }
551
+ async writeArtifact(relativePath, data) {
552
+ const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
553
+ await mkdir(path.dirname(target), { recursive: true });
554
+ await writeFile(target, data);
555
+ }
556
+ async finalize(summary) {
557
+ await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(TrialSummarySchema)(summary));
558
+ await replaceJson(path.join(this.directory, MANIFEST_FILE), {
559
+ ...Schema.encodeSync(TrialMetadataSchema)(this.metadata),
560
+ status: summary.status
561
+ });
562
+ }
563
+ };
564
+ var LocalRunWriter = class {
565
+ location;
566
+ directory;
567
+ metadata;
568
+ constructor(location, directory, metadata) {
569
+ this.location = location;
570
+ this.directory = directory;
571
+ this.metadata = metadata;
572
+ }
573
+ async startTrial(metadata) {
574
+ assertPathSegment(metadata.trialId, "trialId");
575
+ const directory = path.join(this.directory, "trials", metadata.trialId);
576
+ await mkdir(directory, { recursive: true });
577
+ await writeJson(path.join(directory, MANIFEST_FILE), {
578
+ ...Schema.encodeSync(TrialMetadataSchema)(metadata),
579
+ status: "running"
580
+ });
581
+ return new LocalTrialWriter(directory, metadata);
582
+ }
583
+ async finalize(summary) {
584
+ await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
585
+ await replaceJson(path.join(this.directory, MANIFEST_FILE), {
586
+ ...Schema.encodeSync(RunMetadataSchema)(this.metadata),
587
+ status: summary.status
588
+ });
589
+ }
590
+ };
591
+ /** Persists each run as a local tree rooted below `rootDirectory`. */
592
+ function localReportStore(rootDirectory) {
593
+ return { async startRun(metadata) {
594
+ assertPathSegment(metadata.runId, "runId");
595
+ const directory = path.resolve(rootDirectory, metadata.runId);
596
+ await mkdir(directory, { recursive: true });
597
+ await writeJson(path.join(directory, MANIFEST_FILE), {
598
+ ...Schema.encodeSync(RunMetadataSchema)(metadata),
599
+ status: "running"
600
+ });
601
+ return new LocalRunWriter(path.relative(process.cwd(), directory), directory, metadata);
602
+ } };
603
+ }
604
+ //#endregion
605
+ //#region ../runner/src/local-report-reader.ts
606
+ /** Shared v2/v3 local report boundary for CLI, dashboard, and CI consumers. */
607
+ async function decode(file, schema) {
608
+ return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
609
+ }
610
+ const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
611
+ const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
612
+ const readTrialManifest = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "manifest.json"), TrialManifestSchema);
613
+ const readTrialSummary = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "summary.json"), TrialSummarySchema);
614
+ const readTrialScoring = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "scoring.json"), TrialScoringSchema);
615
+ async function readTrialEvents(root, runId, trialId) {
616
+ return parseTrajectoryJsonl(await readFile(join(root, runId, "trials", trialId, "trajectory.jsonl"), "utf8"));
617
+ }
618
+ //#endregion
619
+ //#region ../runner/src/matrix.ts
620
+ /** Bounded workers pull cells from the iterator; no Cartesian job array is allocated. */
621
+ function runMatrix(matrix, options) {
622
+ return Effect.gen(function* () {
623
+ const concurrency = options.concurrency ?? 4;
624
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new Error("Concurrency must be a positive integer");
625
+ const count = matrix.count(options.selection);
626
+ const iterator = matrix.cells(options.selection);
627
+ const summary = {
628
+ cells: 0,
629
+ passed: 0,
630
+ failed: 0
631
+ };
632
+ yield* Effect.all(Array.from({ length: Math.min(count, concurrency) }, () => Effect.gen(function* () {
633
+ while (true) {
634
+ const next = iterator.next();
635
+ if (next.done) return;
636
+ const cell = next.value;
637
+ const runtime = options.runtime ?? [
638
+ "local",
639
+ "remote",
640
+ "sandbox"
641
+ ].find((name) => cell.eval.agent.runtimes?.[name]);
642
+ const result = yield* runEval(cell.eval, {
643
+ ...options,
644
+ concurrency: 1,
645
+ runtime,
646
+ parameters: cell.parameters,
647
+ matrix: {
648
+ id: matrix.id,
649
+ cellKey: cell.key
650
+ }
651
+ });
652
+ summary.cells++;
653
+ if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
654
+ else summary.failed++;
655
+ if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, result));
656
+ }
657
+ })), { concurrency });
658
+ return summary;
659
+ });
660
+ }
661
+ //#endregion
662
+ //#region ../runner/src/index.ts
663
+ /** Effect-native aggregate eval execution entry point. */
664
+ function runEval(definition, options) {
665
+ return Effect.gen(function* () {
666
+ yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
667
+ evalId: authoringId(definition),
668
+ ...options.runId ? { runId: options.runId } : {},
669
+ ...options.suiteId ? { suiteId: options.suiteId } : {}
670
+ }));
671
+ const result = yield* Effect.tryPromise({
672
+ try: () => executeRun(definition, options),
673
+ catch: (error) => error
674
+ });
675
+ yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
676
+ evalId: authoringId(definition),
677
+ runId: result.runId,
678
+ status: result.status
679
+ }));
680
+ return result;
681
+ });
682
+ }
683
+ function createId(_kind) {
684
+ return crypto.randomUUID();
685
+ }
686
+ function canonicalId(kind, id) {
687
+ return resourceUri(kind, id);
688
+ }
689
+ function assertUuid(value, label) {
690
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) throw new Error(`${label} must be a UUID`);
691
+ }
692
+ function runMetadata(definition, options, runId, startedAt) {
693
+ return {
694
+ schemaVersion: 3,
695
+ runId,
696
+ runUri: canonicalId("run", runId),
697
+ evalId: definition.id,
698
+ ...options.suiteId ? { suiteId: options.suiteId } : {},
699
+ ...definition.agent.identity ? { aut: definition.agent.identity } : {},
700
+ parameters: options.parameters,
701
+ matrix: options.matrix,
702
+ startedAt
703
+ };
704
+ }
705
+ /** Recording a secondary error must never mask the original failure or block cleanup. */
706
+ async function tryEmitError(error, emitRunner, now) {
707
+ try {
708
+ await emitRunner({
709
+ kind: "error",
710
+ error: recordError(error),
711
+ timestamp: now().toISOString()
712
+ });
713
+ } catch {}
714
+ }
715
+ /** Executes one aggregate eval run and its requested isolated trials. */
716
+ async function executeRun(definition, options) {
717
+ let hasUser = false;
718
+ for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
719
+ else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
720
+ if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
721
+ if (options.runId) assertUuid(options.runId, "runId");
722
+ if (options.trialId) assertUuid(options.trialId, "trialId");
723
+ const requestedTrials = options.trials ?? definition.policy?.trials ?? 1;
724
+ if (!Number.isInteger(requestedTrials) || requestedTrials < 1) throw new Error(`Trial count must be a positive integer; received ${requestedTrials}`);
725
+ if (requestedTrials === 1) return runTrial(definition, options);
726
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
727
+ const aggregateStartedAt = now();
728
+ const runId = options.runId ?? createId("run");
729
+ const runWriter = await options.report.startRun(runMetadata(definition, options, runId, aggregateStartedAt.toISOString()));
730
+ const trialEffects = Array.from({ length: requestedTrials }, (_, trialIndex) => {
731
+ const trial = Effect.tryPromise({
732
+ try: () => runTrial(definition, {
733
+ ...options,
734
+ runId,
735
+ trialId: createId("trial"),
736
+ trials: 1,
737
+ runWriter,
738
+ trialIndex
739
+ }),
740
+ catch: (error) => error
741
+ });
742
+ return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
743
+ });
744
+ const trials = await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }));
745
+ const failed = trials.filter((trial) => trial.status !== "completed" || !trial.scoring?.passed).length;
746
+ const status = trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
747
+ const endedAt = now();
748
+ await runWriter.finalize({
749
+ status,
750
+ endedAt: endedAt.toISOString(),
751
+ durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
752
+ trialCount: trials.length,
753
+ passed: trials.length - failed,
754
+ failed
755
+ });
756
+ const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
757
+ return {
758
+ ...trials[0],
759
+ runId,
760
+ evalId: authoringId(definition),
761
+ status,
762
+ trialCount: trials.length,
763
+ passed: trials.length - failed,
764
+ failed,
765
+ aggregateScoring: {
766
+ passed: trials.length - failed,
767
+ failed,
768
+ passRate: (trials.length - failed) / trials.length,
769
+ ...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
770
+ },
771
+ trials
772
+ };
773
+ }
774
+ async function runTrial(definition, options) {
775
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
776
+ const runId = options.runId ?? createId("run");
777
+ const trialId = options.trialId ?? createId("trial");
778
+ const startedAt = now().toISOString();
779
+ const selectedRuntime = options.runtime;
780
+ const runtime = selectedRuntime ? definition.agent.runtimes?.[selectedRuntime] : void 0;
781
+ if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
782
+ const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
783
+ const fixtureContext = {
784
+ runId,
785
+ evalId: authoringId(definition),
786
+ trialId,
787
+ trialIndex: options.trialIndex ?? 0,
788
+ metadata: {
789
+ ...definition.metadata ?? {},
790
+ randomSeed
791
+ },
792
+ ...options.parameters ? { parameters: options.parameters } : {},
793
+ ...selectedRuntime ? { runtime: selectedRuntime } : {}
794
+ };
795
+ const events = [];
796
+ const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt));
797
+ const trialWriter = await runWriter.startTrial({
798
+ schemaVersion: 3,
799
+ runId,
800
+ runUri: canonicalId("run", runId),
801
+ trialId,
802
+ trialUri: canonicalId("trial", trialId),
803
+ trialIndex: options.trialIndex ?? 0,
804
+ evalId: definition.id,
805
+ ...definition.agent.identity ? { aut: definition.agent.identity } : {},
806
+ parameters: options.parameters,
807
+ matrix: options.matrix,
808
+ startedAt
809
+ });
810
+ let writeChain = Promise.resolve();
811
+ let reportingFailed = false;
812
+ const append = async (event) => {
813
+ events.push(event);
814
+ writeChain = writeChain.then(() => trialWriter.appendEvent(event));
815
+ try {
816
+ await writeChain;
817
+ } catch (cause) {
818
+ reportingFailed = true;
819
+ throw new ReportError({
820
+ cause,
821
+ message: cause instanceof Error ? cause.message : String(cause)
822
+ });
823
+ }
824
+ };
825
+ const emitRunner = (event) => append({
826
+ ...event,
827
+ source: "runner"
828
+ });
829
+ const onAutEvent = (event) => append({
830
+ ...event,
831
+ source: "aut"
832
+ });
833
+ let workspace;
834
+ let context;
835
+ let primaryError;
836
+ let closeError;
837
+ let scoring;
838
+ let artifacts;
839
+ let status = "running";
840
+ const progress = {
841
+ checkpoints: [],
842
+ stoppedEarly: false
843
+ };
844
+ let cleanupError;
845
+ const workspaceScope = await Effect.runPromise(Scope.make());
846
+ try {
847
+ await emitRunner({
848
+ kind: "trial-started",
849
+ timestamp: now().toISOString()
850
+ });
851
+ workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
852
+ try: () => createTrialWorkspace(fixtureContext, definition.fixtures, options.workspaceRoot),
853
+ catch: (cause) => new FixtureError({
854
+ cause,
855
+ message: cause instanceof Error ? cause.message : String(cause)
856
+ })
857
+ }), (created) => Effect.tryPromise({
858
+ try: () => created.cleanup(),
859
+ catch: (cause) => new FixtureError({
860
+ cause,
861
+ message: cause instanceof Error ? cause.message : String(cause)
862
+ })
863
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
864
+ cleanupError = error;
865
+ }).pipe(Effect.tap(() => Effect.logError("trial workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
866
+ const trialWorkspace = workspace;
867
+ const trialContext = {
868
+ ...fixtureContext,
869
+ workspace: trialWorkspace.artifacts.candidate,
870
+ evaluatorWorkspace: trialWorkspace.artifacts.evaluator
871
+ };
872
+ context = trialContext;
873
+ await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
874
+ try: () => definition.agent.start({
875
+ context: trialContext,
876
+ ...runtime ? { runtime } : {},
877
+ onEvent: onAutEvent
878
+ }),
879
+ catch: (cause) => new AutExecutionError({
880
+ cause,
881
+ message: cause instanceof Error ? cause.message : String(cause)
882
+ })
883
+ }), (session) => Effect.tryPromise({
884
+ try: () => executeTranscript(definition, session, {
885
+ context: trialContext,
886
+ artifacts: trialWorkspace.artifacts,
887
+ events,
888
+ now,
889
+ randomSeed,
890
+ emitRunner,
891
+ progress
892
+ }),
893
+ catch: (cause) => cause
894
+ }), (opened) => Effect.tryPromise({
895
+ try: () => opened.close(),
896
+ catch: (cause) => new AutExecutionError({
897
+ cause,
898
+ message: cause instanceof Error ? cause.message : String(cause)
899
+ })
900
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
901
+ closeError = error;
902
+ }).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
903
+ runId,
904
+ trialId,
905
+ evalId: definition.id
906
+ })));
907
+ } catch (error) {
908
+ primaryError = error;
909
+ await tryEmitError(error, emitRunner, now);
910
+ }
911
+ if (closeError) {
912
+ if (!primaryError) primaryError = closeError;
913
+ await tryEmitError(closeError, emitRunner, now);
914
+ }
915
+ const { checkpoints, stoppedEarly, lastTurn } = progress;
916
+ const scoreResults = [];
917
+ const skippedScorers = [];
918
+ try {
919
+ await evaluateFinalScorers(definition, {
920
+ context,
921
+ workspace,
922
+ events,
923
+ lastTurn,
924
+ primaryError,
925
+ stoppedEarly,
926
+ reportingFailed,
927
+ results: scoreResults,
928
+ skippedScorers,
929
+ now,
930
+ emitRunner
931
+ });
932
+ scoring = scoreSummary(scoreResults, checkpoints, skippedScorers);
933
+ await trialWriter.writeScores(scoring);
934
+ if (workspace) try {
935
+ artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter);
936
+ } catch (captureError) {
937
+ if (!primaryError) primaryError = captureError;
938
+ await tryEmitError(captureError, emitRunner, now);
939
+ }
940
+ } catch (error) {
941
+ if (!primaryError) primaryError = error;
942
+ await tryEmitError(error, emitRunner, now);
943
+ } finally {
944
+ await Effect.runPromise(Scope.close(workspaceScope, Exit.succeed(void 0)));
945
+ if (cleanupError) {
946
+ if (!primaryError) primaryError = cleanupError;
947
+ await tryEmitError(cleanupError, emitRunner, now);
948
+ }
949
+ }
950
+ scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers);
951
+ if (!primaryError) try {
952
+ await emitRunner({
953
+ kind: "trial-completed",
954
+ timestamp: now().toISOString()
955
+ });
956
+ } catch (error) {
957
+ primaryError = error;
958
+ }
959
+ status = primaryError ? "failed" : "completed";
960
+ const error = primaryError ? recordError(primaryError) : void 0;
961
+ const endedAt = now().toISOString();
962
+ await trialWriter.finalize({
963
+ status,
964
+ endedAt,
965
+ durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
966
+ scoring,
967
+ ...artifacts ? { artifacts } : {},
968
+ ...error ? { error } : {}
969
+ });
970
+ if (!options.runWriter) await runWriter.finalize({
971
+ status,
972
+ endedAt,
973
+ trialCount: 1,
974
+ passed: status === "completed" && scoring.passed ? 1 : 0,
975
+ failed: status === "failed" || !scoring.passed ? 1 : 0,
976
+ ...error ? { error } : {}
977
+ });
978
+ return {
979
+ runId,
980
+ runUri: canonicalId("run", runId),
981
+ trialId,
982
+ trialUri: canonicalId("trial", trialId),
983
+ trialIndex: options.trialIndex ?? 0,
984
+ evalId: definition.id,
985
+ status,
986
+ reportLocation: runWriter.location,
987
+ durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
988
+ scoring,
989
+ ...error ? { error } : {}
990
+ };
991
+ }
992
+ //#endregion
993
+ export { readTrialEvents as a, readTrialSummary as c, CheckpointExecutionError as d, FixtureError as f, readRunSummary as i, localReportStore as l, ScoringError as m, runMatrix as n, readTrialManifest as o, ReportError as p, readRunManifest as r, readTrialScoring as s, runEval as t, AutExecutionError as u };
994
+
995
+ //# sourceMappingURL=src-DJDem6nZ.mjs.map