@assemble-dev/evals 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1138 @@
1
+ // src/eval-case.ts
2
+ import { z } from "zod";
3
+ var ExpectedToolCallSchema = z.object({
4
+ toolName: z.string().min(1),
5
+ count: z.number().int().min(1).optional()
6
+ });
7
+ var ExpectedApprovalDecisionSchema = z.object({
8
+ toolName: z.string().min(1).optional(),
9
+ status: z.enum(["approved", "rejected"])
10
+ });
11
+
12
+ // src/eval-suite.ts
13
+ import { AppErrorCode } from "@assemble-dev/shared-types/errors";
14
+ import { AppError } from "@assemble-dev/shared-utils/errors";
15
+
16
+ // src/schema-validation-formatting.ts
17
+ function formatSchemaValidationError(error) {
18
+ if (error.issues === void 0 || error.issues.length === 0) {
19
+ return error.message;
20
+ }
21
+ return error.issues.map(formatSchemaValidationIssue).join("; ");
22
+ }
23
+ function formatSchemaValidationIssue(issue) {
24
+ if (issue.path === void 0 || issue.path.length === 0) {
25
+ return issue.message;
26
+ }
27
+ return `${issue.path.map(String).join(".")}: ${issue.message}`;
28
+ }
29
+
30
+ // src/eval-suite.ts
31
+ function evalSuite(config) {
32
+ ensureSuiteNameIsPresent(config.name);
33
+ const rawCases = config.cases;
34
+ ensureSuiteHasCases(config.name, rawCases);
35
+ ensureCaseNamesAreUnique(config.name, rawCases);
36
+ const validatedCases = rawCases.map(
37
+ (evalCase) => buildValidatedEvalCase(config.workflow, evalCase)
38
+ );
39
+ return Object.freeze({
40
+ name: config.name,
41
+ workflow: config.workflow,
42
+ cases: Object.freeze(validatedCases)
43
+ });
44
+ }
45
+ function ensureSuiteNameIsPresent(suiteName) {
46
+ if (suiteName.trim() !== "") {
47
+ return;
48
+ }
49
+ throw new AppError({
50
+ code: AppErrorCode.ValidationFailed,
51
+ message: "eval suite name must not be empty",
52
+ statusCode: 422
53
+ });
54
+ }
55
+ function ensureSuiteHasCases(suiteName, cases) {
56
+ if (cases.length > 0) {
57
+ return;
58
+ }
59
+ throw new AppError({
60
+ code: AppErrorCode.ValidationFailed,
61
+ message: `eval suite "${suiteName}" must define at least one case`,
62
+ statusCode: 422
63
+ });
64
+ }
65
+ function ensureCaseNamesAreUnique(suiteName, cases) {
66
+ const seenCaseNames = /* @__PURE__ */ new Set();
67
+ for (const evalCase of cases) {
68
+ if (evalCase.name.trim() === "") {
69
+ throw new AppError({
70
+ code: AppErrorCode.ValidationFailed,
71
+ message: `eval suite "${suiteName}" contains a case with an empty name`,
72
+ statusCode: 422
73
+ });
74
+ }
75
+ if (seenCaseNames.has(evalCase.name)) {
76
+ throw new AppError({
77
+ code: AppErrorCode.ValidationFailed,
78
+ message: `eval suite "${suiteName}" contains duplicate case name "${evalCase.name}"`,
79
+ statusCode: 422
80
+ });
81
+ }
82
+ seenCaseNames.add(evalCase.name);
83
+ }
84
+ }
85
+ function buildValidatedEvalCase(workflowDefinition, evalCase) {
86
+ const validatedInput = parseEvalCaseInput(workflowDefinition, evalCase);
87
+ const validatedExpectation = buildValidatedExpectation(
88
+ workflowDefinition,
89
+ evalCase
90
+ );
91
+ if (validatedExpectation === void 0) {
92
+ return Object.freeze({ name: evalCase.name, input: validatedInput });
93
+ }
94
+ return Object.freeze({
95
+ name: evalCase.name,
96
+ input: validatedInput,
97
+ expected: validatedExpectation
98
+ });
99
+ }
100
+ function parseEvalCaseInput(workflowDefinition, evalCase) {
101
+ const parsedInput = workflowDefinition.inputSchema.safeParse(evalCase.input);
102
+ if (!parsedInput.success) {
103
+ throw new AppError({
104
+ code: AppErrorCode.ValidationFailed,
105
+ message: `eval case "${evalCase.name}" input failed workflow "${workflowDefinition.name}" input schema validation: ${formatSchemaValidationError(parsedInput.error)}`,
106
+ statusCode: 422
107
+ });
108
+ }
109
+ return parsedInput.data;
110
+ }
111
+ function buildValidatedExpectation(workflowDefinition, evalCase) {
112
+ const expectation = evalCase.expected;
113
+ if (expectation === void 0) {
114
+ return void 0;
115
+ }
116
+ const validatedExpectation = {};
117
+ if (expectation.output !== void 0) {
118
+ validatedExpectation.output = parseExpectedOutput(
119
+ workflowDefinition,
120
+ evalCase.name,
121
+ expectation.output
122
+ );
123
+ }
124
+ if (expectation.outputFields !== void 0) {
125
+ validatedExpectation.outputFields = { ...expectation.outputFields };
126
+ }
127
+ if (expectation.outputPredicate !== void 0) {
128
+ validatedExpectation.outputPredicate = expectation.outputPredicate;
129
+ }
130
+ if (expectation.toolCalls !== void 0) {
131
+ validatedExpectation.toolCalls = expectation.toolCalls.map((toolCall) => ({
132
+ ...toolCall
133
+ }));
134
+ }
135
+ if (expectation.approvalDecisions !== void 0) {
136
+ validatedExpectation.approvalDecisions = expectation.approvalDecisions.map(
137
+ (approvalDecision) => ({ ...approvalDecision })
138
+ );
139
+ }
140
+ return Object.freeze(validatedExpectation);
141
+ }
142
+ function parseExpectedOutput(workflowDefinition, caseName, expectedOutput) {
143
+ if (workflowDefinition.outputSchema === null) {
144
+ return expectedOutput;
145
+ }
146
+ const parsedOutput = workflowDefinition.outputSchema.safeParse(expectedOutput);
147
+ if (!parsedOutput.success) {
148
+ throw new AppError({
149
+ code: AppErrorCode.ValidationFailed,
150
+ message: `eval case "${caseName}" expected output failed workflow "${workflowDefinition.name}" output schema validation: ${formatSchemaValidationError(parsedOutput.error)}`,
151
+ statusCode: 422
152
+ });
153
+ }
154
+ return parsedOutput.data;
155
+ }
156
+
157
+ // src/dataset-loading.ts
158
+ import { existsSync, readFileSync } from "fs";
159
+ import { AppErrorCode as AppErrorCode2 } from "@assemble-dev/shared-types/errors";
160
+ import { JsonValueSchema } from "@assemble-dev/shared-types/json";
161
+ import { AppError as AppError2 } from "@assemble-dev/shared-utils/errors";
162
+ import { z as z2 } from "zod";
163
+ var SerializedEvalCaseExpectationSchema = z2.strictObject({
164
+ output: JsonValueSchema.optional(),
165
+ outputFields: z2.record(z2.string(), JsonValueSchema).optional(),
166
+ toolCalls: z2.array(ExpectedToolCallSchema).optional(),
167
+ approvalDecisions: z2.array(ExpectedApprovalDecisionSchema).optional()
168
+ });
169
+ var SerializedEvalCaseSchema = z2.strictObject({
170
+ name: z2.string().min(1),
171
+ input: JsonValueSchema,
172
+ expected: SerializedEvalCaseExpectationSchema.optional()
173
+ });
174
+ function loadEvalCasesFromJsonlFile(input) {
175
+ const fileContents = readEvalDatasetFile(input.filePath);
176
+ const lines = fileContents.split(/\r?\n/);
177
+ const cases = [];
178
+ for (const [lineIndex, line] of lines.entries()) {
179
+ if (line.trim() === "") {
180
+ continue;
181
+ }
182
+ const lineNumber = lineIndex + 1;
183
+ const lineValue = parseJsonlLine(line, lineNumber, input.filePath);
184
+ cases.push(parseSerializedEvalCase(lineValue, lineNumber, input.filePath));
185
+ }
186
+ return cases;
187
+ }
188
+ function loadEvalCases(source) {
189
+ if ("jsonlFilePath" in source) {
190
+ return loadEvalCasesFromJsonlFile({ filePath: source.jsonlFilePath });
191
+ }
192
+ return source;
193
+ }
194
+ function readEvalDatasetFile(filePath) {
195
+ if (!existsSync(filePath)) {
196
+ throw new AppError2({
197
+ code: AppErrorCode2.NotFound,
198
+ message: `eval dataset file not found: ${filePath}`,
199
+ statusCode: 404
200
+ });
201
+ }
202
+ return readFileSync(filePath, "utf8");
203
+ }
204
+ function parseJsonlLine(line, lineNumber, filePath) {
205
+ try {
206
+ return JsonValueSchema.parse(JSON.parse(line));
207
+ } catch (error) {
208
+ throw new AppError2({
209
+ code: AppErrorCode2.ValidationFailed,
210
+ message: `invalid JSON on line ${lineNumber} of ${filePath}`,
211
+ statusCode: 422,
212
+ cause: error instanceof Error ? error : void 0
213
+ });
214
+ }
215
+ }
216
+ function parseSerializedEvalCase(lineValue, lineNumber, filePath) {
217
+ const parsedCase = SerializedEvalCaseSchema.safeParse(lineValue);
218
+ if (!parsedCase.success) {
219
+ throw new AppError2({
220
+ code: AppErrorCode2.ValidationFailed,
221
+ message: `invalid eval case on line ${lineNumber} of ${filePath}: ${formatSchemaValidationError(parsedCase.error)}`,
222
+ statusCode: 422
223
+ });
224
+ }
225
+ return parsedCase.data;
226
+ }
227
+
228
+ // src/judge/judge-metric.ts
229
+ import { z as z3 } from "zod";
230
+
231
+ // src/metrics/metric-result.ts
232
+ function createPassedMetricResult(name, reason) {
233
+ return { name, passed: true, score: 1, reason, skipped: false };
234
+ }
235
+ function createFailedMetricResult(name, score, reason) {
236
+ return { name, passed: false, score, reason, skipped: false };
237
+ }
238
+ function createSkippedMetricResult(name, reason) {
239
+ return { name, passed: false, score: 0, reason, skipped: true };
240
+ }
241
+ function createScoredMetricResult(name, outcomes) {
242
+ const satisfiedCount = outcomes.filter(
243
+ (outcome) => outcome.satisfied
244
+ ).length;
245
+ const reason = outcomes.map((outcome) => outcome.reason).join("; ");
246
+ if (satisfiedCount === outcomes.length) {
247
+ return createPassedMetricResult(name, reason);
248
+ }
249
+ return createFailedMetricResult(
250
+ name,
251
+ satisfiedCount / outcomes.length,
252
+ reason
253
+ );
254
+ }
255
+
256
+ // src/judge/judge-metric.ts
257
+ var defaultJudgeMetricName = "llm-judge";
258
+ var defaultJudgePassThreshold = 0.7;
259
+ var JudgeVerdictSchema = z3.object({
260
+ score: z3.number().min(0).max(1),
261
+ reasoning: z3.string().min(1)
262
+ });
263
+ function defineJudgeMetric(config) {
264
+ return {
265
+ name: config.name ?? defaultJudgeMetricName,
266
+ model: config.model,
267
+ rubric: config.rubric,
268
+ passThreshold: config.passThreshold ?? defaultJudgePassThreshold
269
+ };
270
+ }
271
+ async function evaluateJudgeMetric(input) {
272
+ if (input.runState.status !== "completed") {
273
+ return createSkippedMetricResult(
274
+ input.definition.name,
275
+ `judge skipped because run finished with status "${input.runState.status}" instead of "completed"`
276
+ );
277
+ }
278
+ try {
279
+ const structuredOutput = await input.definition.model.generateStructuredOutput({
280
+ messages: buildJudgeChatMessages(input),
281
+ schema: JudgeVerdictSchema
282
+ });
283
+ return mapJudgeVerdictToMetricResult(
284
+ input.definition,
285
+ structuredOutput.value
286
+ );
287
+ } catch (caughtError) {
288
+ const failureReason = caughtError instanceof Error ? caughtError.message : String(caughtError);
289
+ return createFailedMetricResult(
290
+ input.definition.name,
291
+ 0,
292
+ `judge model call failed: ${failureReason}`
293
+ );
294
+ }
295
+ }
296
+ function mapJudgeVerdictToMetricResult(definition, verdict) {
297
+ return {
298
+ name: definition.name,
299
+ passed: verdict.score >= definition.passThreshold,
300
+ score: verdict.score,
301
+ reason: verdict.reasoning,
302
+ skipped: false
303
+ };
304
+ }
305
+ function buildJudgeChatMessages(input) {
306
+ return [
307
+ { role: "system", content: buildJudgeSystemPrompt(input.definition.rubric) },
308
+ {
309
+ role: "user",
310
+ content: JSON.stringify(buildJudgeCaseSummary(input), null, 2)
311
+ }
312
+ ];
313
+ }
314
+ function buildJudgeSystemPrompt(rubric) {
315
+ return [
316
+ "You are an impartial judge that evaluates one eval case from an AI workflow run.",
317
+ "Score how well the final output satisfies the rubric, from 0 (fails completely) to 1 (fully satisfies).",
318
+ "Rubric:",
319
+ rubric,
320
+ 'Respond as pure JSON matching {"score": number between 0 and 1, "reasoning": string} with no surrounding text.'
321
+ ].join("\n");
322
+ }
323
+ function buildJudgeCaseSummary(input) {
324
+ return {
325
+ caseName: input.evalCase.name,
326
+ input: input.evalCase.input,
327
+ expectedSummary: buildExpectedSummary(input.evalCase.expected),
328
+ finalOutput: input.runState.finalOutput
329
+ };
330
+ }
331
+ function buildExpectedSummary(expectation) {
332
+ if (expectation === void 0) {
333
+ return null;
334
+ }
335
+ const expectedSummary = {};
336
+ if (expectation.output !== void 0) {
337
+ expectedSummary.output = expectation.output;
338
+ }
339
+ if (expectation.outputFields !== void 0) {
340
+ expectedSummary.outputFields = expectation.outputFields;
341
+ }
342
+ if (expectation.outputPredicate !== void 0) {
343
+ expectedSummary.outputPredicateDefined = true;
344
+ }
345
+ if (expectation.toolCalls !== void 0) {
346
+ expectedSummary.toolCalls = expectation.toolCalls.map((expectedToolCall) => ({
347
+ toolName: expectedToolCall.toolName,
348
+ count: expectedToolCall.count ?? null
349
+ }));
350
+ }
351
+ if (expectation.approvalDecisions !== void 0) {
352
+ expectedSummary.approvalDecisions = expectation.approvalDecisions.map(
353
+ (expectedApprovalDecision) => ({
354
+ toolName: expectedApprovalDecision.toolName ?? null,
355
+ status: expectedApprovalDecision.status
356
+ })
357
+ );
358
+ }
359
+ return expectedSummary;
360
+ }
361
+
362
+ // src/metrics/approval-correctness-metric.ts
363
+ var approvalCorrectnessMetricName = "approval-correctness";
364
+ function evaluateApprovalCorrectnessMetric(input) {
365
+ const expectedApprovalDecisions = input.expectedApprovalDecisions;
366
+ if (expectedApprovalDecisions === void 0 || expectedApprovalDecisions.length === 0) {
367
+ return createSkippedMetricResult(
368
+ approvalCorrectnessMetricName,
369
+ "no approval decision expectations defined for this case"
370
+ );
371
+ }
372
+ const outcomes = expectedApprovalDecisions.map(
373
+ (expectedApprovalDecision) => evaluateExpectedApprovalDecision(expectedApprovalDecision, input.approvals)
374
+ );
375
+ return createScoredMetricResult(approvalCorrectnessMetricName, outcomes);
376
+ }
377
+ function evaluateExpectedApprovalDecision(expectedApprovalDecision, approvals) {
378
+ const candidateApprovals = filterApprovalsByToolName(
379
+ approvals,
380
+ expectedApprovalDecision.toolName
381
+ );
382
+ const approvalScope = describeApprovalScope(
383
+ expectedApprovalDecision.toolName
384
+ );
385
+ if (candidateApprovals.length === 0) {
386
+ return {
387
+ satisfied: false,
388
+ reason: `no approval found ${approvalScope}`
389
+ };
390
+ }
391
+ const hasMatchingStatus = candidateApprovals.some(
392
+ (approval) => approval.status === expectedApprovalDecision.status
393
+ );
394
+ if (hasMatchingStatus) {
395
+ return {
396
+ satisfied: true,
397
+ reason: `approval ${approvalScope} has status "${expectedApprovalDecision.status}"`
398
+ };
399
+ }
400
+ const observedStatuses = candidateApprovals.map((approval) => `"${approval.status}"`).join(", ");
401
+ return {
402
+ satisfied: false,
403
+ reason: `expected approval ${approvalScope} with status "${expectedApprovalDecision.status}" but found ${observedStatuses}`
404
+ };
405
+ }
406
+ function filterApprovalsByToolName(approvals, toolName) {
407
+ if (toolName === void 0) {
408
+ return approvals;
409
+ }
410
+ return approvals.filter((approval) => approval.toolName === toolName);
411
+ }
412
+ function describeApprovalScope(toolName) {
413
+ if (toolName === void 0) {
414
+ return "for the run";
415
+ }
416
+ return `for tool "${toolName}"`;
417
+ }
418
+
419
+ // src/metrics/schema-validity-metric.ts
420
+ var schemaValidityMetricName = "schema-validity";
421
+ function evaluateSchemaValidityMetric(input) {
422
+ if (input.outputSchema === null) {
423
+ return createSkippedMetricResult(
424
+ schemaValidityMetricName,
425
+ "workflow does not define an output schema"
426
+ );
427
+ }
428
+ const parsedOutput = input.outputSchema.safeParse(input.finalOutput);
429
+ if (parsedOutput.success) {
430
+ return createPassedMetricResult(
431
+ schemaValidityMetricName,
432
+ "final output matches the workflow output schema"
433
+ );
434
+ }
435
+ return createFailedMetricResult(
436
+ schemaValidityMetricName,
437
+ 0,
438
+ `final output failed workflow output schema validation: ${formatSchemaValidationError(parsedOutput.error)}`
439
+ );
440
+ }
441
+
442
+ // src/metrics/json-value-equality.ts
443
+ function isJsonObjectValue(value) {
444
+ return typeof value === "object" && value !== null && !Array.isArray(value);
445
+ }
446
+ function areJsonValuesDeeplyEqual(left, right) {
447
+ if (left === right) {
448
+ return true;
449
+ }
450
+ if (Array.isArray(left) && Array.isArray(right)) {
451
+ return areJsonArraysDeeplyEqual(left, right);
452
+ }
453
+ if (isJsonObjectValue(left) && isJsonObjectValue(right)) {
454
+ return areJsonObjectsDeeplyEqual(left, right);
455
+ }
456
+ return false;
457
+ }
458
+ function areJsonArraysDeeplyEqual(left, right) {
459
+ if (left.length !== right.length) {
460
+ return false;
461
+ }
462
+ return left.every(
463
+ (item, index) => areJsonValuesDeeplyEqual(item, right[index] ?? null)
464
+ );
465
+ }
466
+ function areJsonObjectsDeeplyEqual(left, right) {
467
+ const leftKeys = Object.keys(left);
468
+ const rightKeys = Object.keys(right);
469
+ if (leftKeys.length !== rightKeys.length) {
470
+ return false;
471
+ }
472
+ return leftKeys.every(
473
+ (key) => key in right && areJsonValuesDeeplyEqual(left[key] ?? null, right[key] ?? null)
474
+ );
475
+ }
476
+
477
+ // src/metrics/task-success-metric.ts
478
+ var taskSuccessMetricName = "task-success";
479
+ function evaluateTaskSuccessMetric(input) {
480
+ const expectation = input.expectation;
481
+ if (expectation === void 0 || hasNoOutputExpectations(expectation)) {
482
+ return createSkippedMetricResult(
483
+ taskSuccessMetricName,
484
+ "no output expectations defined for this case"
485
+ );
486
+ }
487
+ const failureReasons = [
488
+ ...collectExactOutputFailureReasons(expectation, input.finalOutput),
489
+ ...collectOutputFieldFailureReasons(expectation, input.finalOutput),
490
+ ...collectOutputPredicateFailureReasons(expectation, input.finalOutput)
491
+ ];
492
+ if (failureReasons.length > 0) {
493
+ return createFailedMetricResult(
494
+ taskSuccessMetricName,
495
+ 0,
496
+ failureReasons.join("; ")
497
+ );
498
+ }
499
+ return createPassedMetricResult(
500
+ taskSuccessMetricName,
501
+ "all output expectations matched the final output"
502
+ );
503
+ }
504
+ function hasNoOutputExpectations(expectation) {
505
+ return expectation.output === void 0 && expectation.outputFields === void 0 && expectation.outputPredicate === void 0;
506
+ }
507
+ function collectExactOutputFailureReasons(expectation, finalOutput) {
508
+ if (expectation.output === void 0) {
509
+ return [];
510
+ }
511
+ if (areJsonValuesDeeplyEqual(expectation.output, finalOutput)) {
512
+ return [];
513
+ }
514
+ return [
515
+ `final output ${JSON.stringify(finalOutput)} does not deeply equal expected output ${JSON.stringify(expectation.output)}`
516
+ ];
517
+ }
518
+ function collectOutputFieldFailureReasons(expectation, finalOutput) {
519
+ const outputFields = expectation.outputFields;
520
+ if (outputFields === void 0) {
521
+ return [];
522
+ }
523
+ if (!isJsonObjectValue(finalOutput)) {
524
+ return [
525
+ "final output is not an object, so expected output fields cannot match"
526
+ ];
527
+ }
528
+ const failureReasons = [];
529
+ for (const [fieldName, expectedValue] of Object.entries(outputFields)) {
530
+ if (!(fieldName in finalOutput)) {
531
+ failureReasons.push(
532
+ `expected output field "${fieldName}" is missing from the final output`
533
+ );
534
+ continue;
535
+ }
536
+ if (!areJsonValuesDeeplyEqual(finalOutput[fieldName] ?? null, expectedValue)) {
537
+ failureReasons.push(
538
+ `output field "${fieldName}" is ${JSON.stringify(finalOutput[fieldName])} but expected ${JSON.stringify(expectedValue)}`
539
+ );
540
+ }
541
+ }
542
+ return failureReasons;
543
+ }
544
+ function collectOutputPredicateFailureReasons(expectation, finalOutput) {
545
+ const outputPredicate = expectation.outputPredicate;
546
+ if (outputPredicate === void 0) {
547
+ return [];
548
+ }
549
+ try {
550
+ const predicateResult = outputPredicate(finalOutput);
551
+ if (predicateResult === true) {
552
+ return [];
553
+ }
554
+ if (predicateResult === false) {
555
+ return ["output predicate returned false"];
556
+ }
557
+ return [`output predicate failed: ${predicateResult}`];
558
+ } catch (caughtError) {
559
+ const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError);
560
+ return [`output predicate threw an error: ${errorMessage}`];
561
+ }
562
+ }
563
+
564
+ // src/metrics/tool-accuracy-metric.ts
565
+ var toolAccuracyMetricName = "tool-accuracy";
566
+ function evaluateToolAccuracyMetric(input) {
567
+ const expectedToolCalls = input.expectedToolCalls;
568
+ if (expectedToolCalls === void 0 || expectedToolCalls.length === 0) {
569
+ return createSkippedMetricResult(
570
+ toolAccuracyMetricName,
571
+ "no tool call expectations defined for this case"
572
+ );
573
+ }
574
+ const completedCallCountsByToolName = countCompletedToolCallsByName(
575
+ input.toolCalls
576
+ );
577
+ const outcomes = expectedToolCalls.map(
578
+ (expectedToolCall) => evaluateExpectedToolCall(expectedToolCall, completedCallCountsByToolName)
579
+ );
580
+ return createScoredMetricResult(toolAccuracyMetricName, outcomes);
581
+ }
582
+ function countCompletedToolCallsByName(toolCalls) {
583
+ const completedCallCounts = /* @__PURE__ */ new Map();
584
+ for (const toolCall of toolCalls) {
585
+ if (toolCall.status !== "completed") {
586
+ continue;
587
+ }
588
+ const currentCount = completedCallCounts.get(toolCall.toolName) ?? 0;
589
+ completedCallCounts.set(toolCall.toolName, currentCount + 1);
590
+ }
591
+ return completedCallCounts;
592
+ }
593
+ function evaluateExpectedToolCall(expectedToolCall, completedCallCountsByToolName) {
594
+ const completedCount = completedCallCountsByToolName.get(expectedToolCall.toolName) ?? 0;
595
+ if (expectedToolCall.count !== void 0) {
596
+ return {
597
+ satisfied: completedCount === expectedToolCall.count,
598
+ reason: `tool "${expectedToolCall.toolName}" completed ${completedCount} time(s), expected exactly ${expectedToolCall.count}`
599
+ };
600
+ }
601
+ return {
602
+ satisfied: completedCount >= 1,
603
+ reason: `tool "${expectedToolCall.toolName}" completed ${completedCount} time(s), expected at least 1`
604
+ };
605
+ }
606
+
607
+ // src/metrics/case-metrics.ts
608
+ var runStatusMetricName = "run-status";
609
+ function collectRunPerformance(runState) {
610
+ const tokenUsage = runState.metrics.tokenUsage;
611
+ return {
612
+ totalLatencyMs: runState.metrics.totalLatencyMs,
613
+ tokenUsage: tokenUsage === null ? null : { ...tokenUsage }
614
+ };
615
+ }
616
+ function evaluateCaseMetrics(input) {
617
+ const performance = collectRunPerformance(input.runState);
618
+ if (input.runState.status !== "completed") {
619
+ return {
620
+ caseName: input.evalCase.name,
621
+ metrics: [buildRunStatusFailureMetric(input.runState)],
622
+ passed: false,
623
+ performance
624
+ };
625
+ }
626
+ const metrics = evaluateCompletedRunMetrics(input);
627
+ return {
628
+ caseName: input.evalCase.name,
629
+ metrics,
630
+ passed: metrics.every((metric) => metric.skipped || metric.passed),
631
+ performance
632
+ };
633
+ }
634
+ function buildRunStatusFailureMetric(runState) {
635
+ return createFailedMetricResult(
636
+ runStatusMetricName,
637
+ 0,
638
+ `run finished with status "${runState.status}" instead of "completed"`
639
+ );
640
+ }
641
+ function evaluateCompletedRunMetrics(input) {
642
+ return [
643
+ evaluateTaskSuccessMetric({
644
+ expectation: input.evalCase.expected,
645
+ finalOutput: input.runState.finalOutput
646
+ }),
647
+ evaluateSchemaValidityMetric({
648
+ outputSchema: input.workflow.outputSchema,
649
+ finalOutput: input.runState.finalOutput
650
+ }),
651
+ evaluateToolAccuracyMetric({
652
+ expectedToolCalls: input.evalCase.expected?.toolCalls,
653
+ toolCalls: input.runState.toolCalls
654
+ }),
655
+ evaluateApprovalCorrectnessMetric({
656
+ expectedApprovalDecisions: input.evalCase.expected?.approvalDecisions,
657
+ approvals: input.runState.approvals
658
+ })
659
+ ];
660
+ }
661
+
662
+ // src/metrics/suite-metrics.ts
663
+ function determineEvalSuiteStatus(summary) {
664
+ return summary.failedCases > 0 ? "failed" : "passed";
665
+ }
666
+ function aggregateSuiteMetrics(caseResults) {
667
+ const totalCases = caseResults.length;
668
+ const passedCases = caseResults.filter(
669
+ (caseResult) => caseResult.passed
670
+ ).length;
671
+ return {
672
+ totalCases,
673
+ passedCases,
674
+ failedCases: totalCases - passedCases,
675
+ passRate: totalCases === 0 ? 0 : passedCases / totalCases,
676
+ metricBreakdown: buildMetricBreakdown(caseResults),
677
+ performance: summarizeSuitePerformance(caseResults)
678
+ };
679
+ }
680
+ function buildMetricBreakdown(caseResults) {
681
+ const metricBreakdown = {};
682
+ for (const caseResult of caseResults) {
683
+ for (const metric of caseResult.metrics) {
684
+ const entry = metricBreakdown[metric.name] ?? {
685
+ evaluated: 0,
686
+ passed: 0,
687
+ failed: 0,
688
+ skipped: 0
689
+ };
690
+ if (metric.skipped) {
691
+ entry.skipped += 1;
692
+ } else {
693
+ entry.evaluated += 1;
694
+ if (metric.passed) {
695
+ entry.passed += 1;
696
+ } else {
697
+ entry.failed += 1;
698
+ }
699
+ }
700
+ metricBreakdown[metric.name] = entry;
701
+ }
702
+ }
703
+ return metricBreakdown;
704
+ }
705
+ function summarizeSuitePerformance(caseResults) {
706
+ const latencies = caseResults.map((caseResult) => caseResult.performance.totalLatencyMs).filter((latencyMs) => latencyMs !== null);
707
+ const tokenUsages = caseResults.map((caseResult) => caseResult.performance.tokenUsage).filter((tokenUsage) => tokenUsage !== null);
708
+ const totalLatencyMs = latencies.length === 0 ? null : latencies.reduce((sum, latencyMs) => sum + latencyMs, 0);
709
+ return {
710
+ totalLatencyMs,
711
+ averageLatencyMs: totalLatencyMs === null ? null : totalLatencyMs / latencies.length,
712
+ tokenUsage: tokenUsages.length === 0 ? null : sumTokenUsages(tokenUsages)
713
+ };
714
+ }
715
+ function sumTokenUsages(tokenUsages) {
716
+ return tokenUsages.reduce(
717
+ (summedUsage, tokenUsage) => ({
718
+ inputTokens: summedUsage.inputTokens + tokenUsage.inputTokens,
719
+ outputTokens: summedUsage.outputTokens + tokenUsage.outputTokens,
720
+ totalTokens: summedUsage.totalTokens + tokenUsage.totalTokens
721
+ }),
722
+ { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
723
+ );
724
+ }
725
+
726
+ // src/eval-webhook.ts
727
+ import { z as z5 } from "zod";
728
+
729
+ // src/runner/eval-run-file.ts
730
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
731
+ import { join } from "path";
732
+ import { TokenUsageSchema } from "@assemble-dev/shared-types/runs";
733
+ import { TraceEventSchema } from "@assemble-dev/shared-types/trace-events";
734
+ import { z as z4 } from "zod";
735
+ var defaultEvalResultsDirectory = join(".assemble", "evals");
736
+ var EvalMetricResultSchema = z4.object({
737
+ name: z4.string().min(1),
738
+ passed: z4.boolean(),
739
+ score: z4.number(),
740
+ reason: z4.string(),
741
+ skipped: z4.boolean()
742
+ });
743
+ var EvalCasePerformanceSchema = z4.object({
744
+ totalLatencyMs: z4.number().nonnegative().nullable(),
745
+ tokenUsage: TokenUsageSchema.nullable()
746
+ });
747
+ var EvalCaseResultSchema = z4.object({
748
+ caseName: z4.string().min(1),
749
+ runId: z4.string().min(1),
750
+ passed: z4.boolean(),
751
+ metrics: z4.array(EvalMetricResultSchema),
752
+ performance: EvalCasePerformanceSchema
753
+ });
754
+ var EvalMetricBreakdownSchema = z4.object({
755
+ evaluated: z4.number().int().nonnegative(),
756
+ passed: z4.number().int().nonnegative(),
757
+ failed: z4.number().int().nonnegative(),
758
+ skipped: z4.number().int().nonnegative()
759
+ });
760
+ var EvalSuitePerformanceSummarySchema = z4.object({
761
+ totalLatencyMs: z4.number().nonnegative().nullable(),
762
+ averageLatencyMs: z4.number().nonnegative().nullable(),
763
+ tokenUsage: TokenUsageSchema.nullable()
764
+ });
765
+ var EvalSuiteSummarySchema = z4.object({
766
+ totalCases: z4.number().int().nonnegative(),
767
+ passedCases: z4.number().int().nonnegative(),
768
+ failedCases: z4.number().int().nonnegative(),
769
+ passRate: z4.number().min(0).max(1),
770
+ metricBreakdown: z4.record(z4.string(), EvalMetricBreakdownSchema),
771
+ performance: EvalSuitePerformanceSummarySchema
772
+ });
773
+ var EvalRunFileSchema = z4.object({
774
+ evalRunId: z4.string().min(1),
775
+ suiteName: z4.string().min(1),
776
+ workflowName: z4.string().min(1),
777
+ startedAt: z4.iso.datetime(),
778
+ completedAt: z4.iso.datetime(),
779
+ summary: EvalSuiteSummarySchema,
780
+ caseResults: z4.array(EvalCaseResultSchema),
781
+ events: z4.array(TraceEventSchema)
782
+ });
783
+ function resolveEvalRunFilePath(evalRunId, directory = defaultEvalResultsDirectory) {
784
+ return join(directory, `${evalRunId}.json`);
785
+ }
786
+ function saveEvalRunFile(input) {
787
+ const directory = input.directory ?? defaultEvalResultsDirectory;
788
+ const filePath = resolveEvalRunFilePath(
789
+ input.evalRunFile.evalRunId,
790
+ directory
791
+ );
792
+ const validatedEvalRunFile = EvalRunFileSchema.parse(input.evalRunFile);
793
+ mkdirSync(directory, { recursive: true });
794
+ writeFileSync(
795
+ filePath,
796
+ JSON.stringify(validatedEvalRunFile, null, 2),
797
+ "utf8"
798
+ );
799
+ return filePath;
800
+ }
801
+ function loadEvalRunFile(input) {
802
+ const filePath = resolveEvalRunFilePath(input.evalRunId, input.directory);
803
+ const rawContents = readFileSync2(filePath, "utf8");
804
+ return EvalRunFileSchema.parse(JSON.parse(rawContents));
805
+ }
806
+
807
+ // src/eval-webhook.ts
808
+ var EvalWebhookCaseSchema = z5.object({
809
+ caseName: z5.string().min(1),
810
+ passed: z5.boolean(),
811
+ metrics: z5.array(EvalMetricResultSchema)
812
+ });
813
+ var EvalWebhookPayloadSchema = z5.object({
814
+ suiteName: z5.string().min(1),
815
+ status: z5.enum(["passed", "failed"]),
816
+ totalCases: z5.number().int().nonnegative(),
817
+ passedCases: z5.number().int().nonnegative(),
818
+ failedCases: z5.number().int().nonnegative(),
819
+ cases: z5.array(EvalWebhookCaseSchema)
820
+ });
821
+ function buildEvalWebhookPayload(runResult) {
822
+ return {
823
+ suiteName: runResult.suiteName,
824
+ status: determineEvalSuiteStatus(runResult.summary),
825
+ totalCases: runResult.summary.totalCases,
826
+ passedCases: runResult.summary.passedCases,
827
+ failedCases: runResult.summary.failedCases,
828
+ cases: runResult.caseResults.map((caseResult) => ({
829
+ caseName: caseResult.caseName,
830
+ passed: caseResult.passed,
831
+ metrics: caseResult.metrics
832
+ }))
833
+ };
834
+ }
835
+ async function postEvalResultsToWebhook(input) {
836
+ const validatedPayload = EvalWebhookPayloadSchema.parse(input.payload);
837
+ const webhookFetch = input.fetchImplementation ?? fetch;
838
+ try {
839
+ const response = await webhookFetch(input.webhookUrl, {
840
+ method: "POST",
841
+ headers: { "content-type": "application/json" },
842
+ body: JSON.stringify(validatedPayload)
843
+ });
844
+ if (response.ok) {
845
+ return { outcome: "delivered" };
846
+ }
847
+ return {
848
+ outcome: "failed",
849
+ message: `Webhook responded with status ${response.status}`
850
+ };
851
+ } catch (caughtError) {
852
+ const reason = caughtError instanceof Error ? caughtError.message : String(caughtError);
853
+ return {
854
+ outcome: "failed",
855
+ message: `Webhook request failed: ${reason}`
856
+ };
857
+ }
858
+ }
859
+
860
+ // src/runner/run-eval-suite.ts
861
+ import {
862
+ buildMissingKeyMessage,
863
+ createTraceBus,
864
+ resolvePlatformConfig,
865
+ runWorkflow
866
+ } from "@assemble-dev/sdk";
867
+ import { AppErrorCode as AppErrorCode3 } from "@assemble-dev/shared-types/errors";
868
+ import { AppError as AppError3 } from "@assemble-dev/shared-utils/errors";
869
+ import {
870
+ generateEvalRunId,
871
+ generateRunId,
872
+ generateTraceEventId
873
+ } from "@assemble-dev/shared-utils/ids";
874
+ async function runEvalSuite(input) {
875
+ const context = buildEvalRunContext(input.suite, input.options);
876
+ emitEvalStartedEvent(context);
877
+ const caseResults = await executeEvalSuiteCases(context);
878
+ const summary = aggregateSuiteMetrics(caseResults);
879
+ emitEvalCompletedEvent(context, summary);
880
+ await context.evalTraceBus.flushSinks();
881
+ const evalRunFile = buildEvalRunFile(context, summary, caseResults);
882
+ const resultsFilePath = saveEvalRunFile({
883
+ evalRunFile,
884
+ directory: context.resultsDirectory
885
+ });
886
+ const uploadStatus = await uploadEvalRunFile(context, evalRunFile);
887
+ return {
888
+ evalRunId: context.evalRunId,
889
+ suiteName: context.suite.name,
890
+ summary,
891
+ caseResults,
892
+ resultsFilePath,
893
+ uploadStatus
894
+ };
895
+ }
896
+ function buildEvalRunContext(suite, options) {
897
+ const resolvedOptions = options ?? {};
898
+ const now = resolvedOptions.now ?? (() => /* @__PURE__ */ new Date());
899
+ const startedAtDate = now();
900
+ const caseRunIds = suite.cases.map(() => generateRunId());
901
+ const evalEventsRunId = caseRunIds[0] ?? generateRunId();
902
+ return {
903
+ suite,
904
+ effectiveWorkflow: buildWorkflowWithMockedTools(
905
+ suite.workflow,
906
+ resolvedOptions.toolMocks
907
+ ),
908
+ options: resolvedOptions,
909
+ evalRunId: resolvedOptions.evalRunId ?? generateEvalRunId(),
910
+ caseRunIds,
911
+ evalEventsRunId,
912
+ now,
913
+ startedAt: startedAtDate.toISOString(),
914
+ startedAtMs: startedAtDate.getTime(),
915
+ evalTraceBus: createTraceBus({
916
+ runId: evalEventsRunId,
917
+ workflowName: suite.workflow.name,
918
+ input: null,
919
+ sinks: resolvedOptions.sinks ?? [],
920
+ now
921
+ }),
922
+ resultsDirectory: resolvedOptions.resultsDirectory ?? defaultEvalResultsDirectory
923
+ };
924
+ }
925
+ function buildWorkflowWithMockedTools(workflowDefinition, toolMocks) {
926
+ if (toolMocks === void 0 || Object.keys(toolMocks).length === 0) {
927
+ return workflowDefinition;
928
+ }
929
+ ensureToolMocksMatchRegisteredTools(workflowDefinition, toolMocks);
930
+ const toolsWithMocks = workflowDefinition.tools.map((workflowTool) => {
931
+ const toolMock = toolMocks[workflowTool.name];
932
+ if (toolMock === void 0) {
933
+ return workflowTool;
934
+ }
935
+ return {
936
+ ...workflowTool,
937
+ execute: async (toolInput) => await Promise.resolve(toolMock(toolInput))
938
+ };
939
+ });
940
+ return { ...workflowDefinition, tools: toolsWithMocks };
941
+ }
942
+ function ensureToolMocksMatchRegisteredTools(workflowDefinition, toolMocks) {
943
+ const registeredToolNames = new Set(
944
+ workflowDefinition.tools.map((workflowTool) => workflowTool.name)
945
+ );
946
+ const unknownToolNames = Object.keys(toolMocks).filter(
947
+ (mockedToolName) => !registeredToolNames.has(mockedToolName)
948
+ );
949
+ if (unknownToolNames.length === 0) {
950
+ return;
951
+ }
952
+ throw new AppError3({
953
+ code: AppErrorCode3.ValidationFailed,
954
+ message: `toolMocks reference tools not registered on workflow "${workflowDefinition.name}": ${unknownToolNames.join(", ")}`,
955
+ statusCode: 422
956
+ });
957
+ }
958
+ async function executeEvalSuiteCases(context) {
959
+ const caseResults = [];
960
+ for (const [caseIndex, evalCase] of context.suite.cases.entries()) {
961
+ const runId = context.caseRunIds[caseIndex] ?? generateRunId();
962
+ const runResult = await runWorkflow({
963
+ definition: context.effectiveWorkflow,
964
+ input: evalCase.input,
965
+ options: {
966
+ runId,
967
+ now: context.options.now,
968
+ sinks: context.options.sinks,
969
+ persistRunState: false
970
+ }
971
+ });
972
+ const caseMetrics = evaluateCaseMetrics({
973
+ runState: runResult.state,
974
+ evalCase,
975
+ workflow: context.suite.workflow
976
+ });
977
+ const metrics = await appendJudgeMetricResults(
978
+ context.options.judges,
979
+ evalCase,
980
+ runResult.state,
981
+ caseMetrics.metrics
982
+ );
983
+ caseResults.push({
984
+ caseName: caseMetrics.caseName,
985
+ runId,
986
+ passed: metrics.every((metric) => metric.skipped || metric.passed),
987
+ metrics,
988
+ performance: caseMetrics.performance
989
+ });
990
+ }
991
+ return caseResults;
992
+ }
993
+ async function appendJudgeMetricResults(judges, evalCase, runState, metrics) {
994
+ if (judges === void 0 || judges.length === 0) {
995
+ return metrics;
996
+ }
997
+ const judgeResults = [];
998
+ for (const definition of judges) {
999
+ judgeResults.push(
1000
+ await evaluateJudgeMetric({ definition, evalCase, runState })
1001
+ );
1002
+ }
1003
+ return [...metrics, ...judgeResults];
1004
+ }
1005
+ function emitEvalStartedEvent(context) {
1006
+ context.evalTraceBus.emitTraceEvent({
1007
+ id: generateTraceEventId(),
1008
+ runId: context.evalEventsRunId,
1009
+ workflowName: context.suite.workflow.name,
1010
+ timestamp: context.now().toISOString(),
1011
+ parentEventId: null,
1012
+ redacted: false,
1013
+ type: "eval.started",
1014
+ evalRunId: context.evalRunId,
1015
+ suiteName: context.suite.name,
1016
+ caseCount: context.suite.cases.length
1017
+ });
1018
+ }
1019
+ function emitEvalCompletedEvent(context, summary) {
1020
+ context.evalTraceBus.emitTraceEvent({
1021
+ id: generateTraceEventId(),
1022
+ runId: context.evalEventsRunId,
1023
+ workflowName: context.suite.workflow.name,
1024
+ timestamp: context.now().toISOString(),
1025
+ parentEventId: null,
1026
+ redacted: false,
1027
+ type: "eval.completed",
1028
+ evalRunId: context.evalRunId,
1029
+ passedCaseCount: summary.passedCases,
1030
+ failedCaseCount: summary.failedCases,
1031
+ latencyMs: Math.max(0, context.now().getTime() - context.startedAtMs)
1032
+ });
1033
+ }
1034
+ function buildEvalRunFile(context, summary, caseResults) {
1035
+ return {
1036
+ evalRunId: context.evalRunId,
1037
+ suiteName: context.suite.name,
1038
+ workflowName: context.suite.workflow.name,
1039
+ startedAt: context.startedAt,
1040
+ completedAt: context.now().toISOString(),
1041
+ summary,
1042
+ caseResults,
1043
+ events: [...context.evalTraceBus.listTraceEvents()]
1044
+ };
1045
+ }
1046
+ function resolveEvalRunsUploadUrl(baseUrl) {
1047
+ return `${baseUrl}/v1/eval-runs`;
1048
+ }
1049
+ function writeEvalLineToStdout(line) {
1050
+ process.stdout.write(`${line}
1051
+ `);
1052
+ }
1053
+ async function uploadEvalRunFile(context, evalRunFile) {
1054
+ const platformOptions = context.options.platform ?? {};
1055
+ const platformConfig = resolvePlatformConfig({
1056
+ apiKey: platformOptions.apiKey,
1057
+ baseUrl: platformOptions.baseUrl,
1058
+ environmentVariables: platformOptions.environmentVariables,
1059
+ homeDirectory: platformOptions.homeDirectory
1060
+ });
1061
+ if (platformConfig.apiKey === null) {
1062
+ const writeLine = context.options.writeLine ?? writeEvalLineToStdout;
1063
+ writeLine(buildMissingKeyMessage());
1064
+ return "skipped-no-key";
1065
+ }
1066
+ return await postEvalRunFileToPlatform(
1067
+ platformConfig.apiKey,
1068
+ platformConfig.baseUrl,
1069
+ evalRunFile,
1070
+ platformOptions.fetchImplementation
1071
+ );
1072
+ }
1073
+ async function postEvalRunFileToPlatform(apiKey, baseUrl, evalRunFile, fetchImplementation) {
1074
+ const uploadFetch = fetchImplementation ?? fetch;
1075
+ try {
1076
+ const response = await uploadFetch(resolveEvalRunsUploadUrl(baseUrl), {
1077
+ method: "POST",
1078
+ headers: {
1079
+ authorization: `Bearer ${apiKey}`,
1080
+ "content-type": "application/json"
1081
+ },
1082
+ body: JSON.stringify(evalRunFile)
1083
+ });
1084
+ return response.ok ? "uploaded" : "failed";
1085
+ } catch {
1086
+ return "failed";
1087
+ }
1088
+ }
1089
+ export {
1090
+ EvalCasePerformanceSchema,
1091
+ EvalCaseResultSchema,
1092
+ EvalMetricBreakdownSchema,
1093
+ EvalMetricResultSchema,
1094
+ EvalRunFileSchema,
1095
+ EvalSuitePerformanceSummarySchema,
1096
+ EvalSuiteSummarySchema,
1097
+ EvalWebhookCaseSchema,
1098
+ EvalWebhookPayloadSchema,
1099
+ ExpectedApprovalDecisionSchema,
1100
+ ExpectedToolCallSchema,
1101
+ JudgeVerdictSchema,
1102
+ SerializedEvalCaseExpectationSchema,
1103
+ SerializedEvalCaseSchema,
1104
+ aggregateSuiteMetrics,
1105
+ approvalCorrectnessMetricName,
1106
+ areJsonValuesDeeplyEqual,
1107
+ buildEvalWebhookPayload,
1108
+ collectRunPerformance,
1109
+ createFailedMetricResult,
1110
+ createPassedMetricResult,
1111
+ createScoredMetricResult,
1112
+ createSkippedMetricResult,
1113
+ defaultEvalResultsDirectory,
1114
+ defaultJudgeMetricName,
1115
+ defaultJudgePassThreshold,
1116
+ defineJudgeMetric,
1117
+ determineEvalSuiteStatus,
1118
+ evalSuite,
1119
+ evaluateApprovalCorrectnessMetric,
1120
+ evaluateCaseMetrics,
1121
+ evaluateJudgeMetric,
1122
+ evaluateSchemaValidityMetric,
1123
+ evaluateTaskSuccessMetric,
1124
+ evaluateToolAccuracyMetric,
1125
+ isJsonObjectValue,
1126
+ loadEvalCases,
1127
+ loadEvalCasesFromJsonlFile,
1128
+ loadEvalRunFile,
1129
+ postEvalResultsToWebhook,
1130
+ resolveEvalRunFilePath,
1131
+ resolveEvalRunsUploadUrl,
1132
+ runEvalSuite,
1133
+ runStatusMetricName,
1134
+ saveEvalRunFile,
1135
+ schemaValidityMetricName,
1136
+ taskSuccessMetricName,
1137
+ toolAccuracyMetricName
1138
+ };