@agent-inspect/eval 2.3.0 → 2.5.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.mjs CHANGED
@@ -1,4 +1,914 @@
1
1
  import { openTrace, TraceReadError } from 'agent-inspect/readers';
2
+ import crypto, { createHash } from 'crypto';
3
+
4
+ // packages/eval/src/index.ts
5
+ var ALL_RULES = [
6
+ "circuit.same-tool-repetition",
7
+ "circuit.same-args-repetition",
8
+ "circuit.max-loop-iterations",
9
+ "circuit.max-retries",
10
+ "circuit.tool-timeout",
11
+ "circuit.runaway-llm-loop",
12
+ "circuit.excessive-branch-width"
13
+ ];
14
+ function closed(ruleId, message) {
15
+ return { ruleId, status: "closed", severity: "info", message, evidence: [] };
16
+ }
17
+ function open(ruleId, message, evidence, severity = "error") {
18
+ return {
19
+ ruleId,
20
+ status: severity === "warning" ? "warn" : "open",
21
+ severity,
22
+ message,
23
+ evidence
24
+ };
25
+ }
26
+ function isToolEvent(event) {
27
+ const name = event.name.toLowerCase();
28
+ return event.kind === "tool" || name.startsWith("tool:") || name.startsWith("function:") || name.includes(".tool.") || name.startsWith("mcp:");
29
+ }
30
+ function isLlmEvent(event) {
31
+ const name = event.name.toLowerCase();
32
+ return event.kind === "llm" || name.startsWith("llm:") || name.includes(".llm.") || name.includes("generation");
33
+ }
34
+ function toolLabel(event) {
35
+ const attrs = event.attributes ?? {};
36
+ const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;
37
+ if (typeof fromAttr === "string" && fromAttr.length > 0) return fromAttr;
38
+ return event.name.replace(/^(tool:|function:|mcp:)/i, "");
39
+ }
40
+ function argsHash(toolName, args) {
41
+ return createHash("sha256").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest("hex").slice(0, 16);
42
+ }
43
+ function toolArgs(event) {
44
+ const attrs = event.attributes ?? {};
45
+ return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;
46
+ }
47
+ function durationMs(event) {
48
+ if (typeof event.durationMs === "number") return event.durationMs;
49
+ const attrs = event.attributes ?? {};
50
+ const fromAttr = attrs.durationMs ?? attrs.duration;
51
+ return typeof fromAttr === "number" ? fromAttr : void 0;
52
+ }
53
+ function attemptNumber(event) {
54
+ const attrs = event.attributes ?? {};
55
+ const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;
56
+ return typeof value === "number" ? value : void 0;
57
+ }
58
+ function evaluateSameToolRepetition(events, maxRepeats) {
59
+ const ruleId = "circuit.same-tool-repetition";
60
+ const counts = /* @__PURE__ */ new Map();
61
+ for (const event of events.filter(isToolEvent)) {
62
+ const label = toolLabel(event);
63
+ counts.set(label, (counts.get(label) ?? 0) + 1);
64
+ }
65
+ const evidence = [];
66
+ for (const [toolName, count] of counts) {
67
+ if (count > maxRepeats) {
68
+ evidence.push({ ruleId, toolName, count, threshold: maxRepeats });
69
+ }
70
+ }
71
+ if (evidence.length === 0) {
72
+ return closed(ruleId, "Tool repetition within threshold.");
73
+ }
74
+ return open(ruleId, "Same tool repeated beyond threshold.", evidence);
75
+ }
76
+ function evaluateSameArgsRepetition(events, maxRepeats) {
77
+ const ruleId = "circuit.same-args-repetition";
78
+ const counts = /* @__PURE__ */ new Map();
79
+ for (const event of events.filter(isToolEvent)) {
80
+ const label = toolLabel(event);
81
+ const hash = argsHash(label, toolArgs(event));
82
+ const key = `${label}:${hash}`;
83
+ const current = counts.get(key) ?? { toolName: label, count: 0 };
84
+ current.count += 1;
85
+ counts.set(key, current);
86
+ }
87
+ const evidence = [];
88
+ for (const entry of counts.values()) {
89
+ if (entry.count > maxRepeats) {
90
+ evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });
91
+ }
92
+ }
93
+ if (evidence.length === 0) {
94
+ return closed(ruleId, "Tool argument repetition within threshold.");
95
+ }
96
+ return open(ruleId, "Same tool arguments repeated beyond threshold.", evidence);
97
+ }
98
+ function evaluateMaxLoopIterations(events, maxIterations) {
99
+ const ruleId = "circuit.max-loop-iterations";
100
+ const iterationEvents = events.filter((event) => {
101
+ const attrs = event.attributes ?? {};
102
+ return typeof attrs.iteration === "number" || event.name.toLowerCase().includes("loop");
103
+ });
104
+ const maxSeen = iterationEvents.reduce((max, event) => {
105
+ const attrs = event.attributes ?? {};
106
+ const iteration = typeof attrs.iteration === "number" ? attrs.iteration : max;
107
+ return Math.max(max, iteration);
108
+ }, iterationEvents.length);
109
+ if (maxSeen <= maxIterations) {
110
+ return closed(ruleId, "Loop iterations within threshold.");
111
+ }
112
+ return open(ruleId, "Loop iterations exceeded threshold.", [
113
+ { ruleId, count: maxSeen, threshold: maxIterations }
114
+ ]);
115
+ }
116
+ function evaluateMaxRetries(events, maxRetries) {
117
+ const ruleId = "circuit.max-retries";
118
+ const attempts = events.map(attemptNumber).filter((value) => value !== void 0);
119
+ const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;
120
+ if (maxAttempt <= maxRetries) {
121
+ return closed(ruleId, "Retry count within threshold.");
122
+ }
123
+ return open(ruleId, "Retry count exceeded threshold.", [
124
+ { ruleId, count: maxAttempt, threshold: maxRetries }
125
+ ]);
126
+ }
127
+ function evaluateToolTimeout(events, maxDurationMs) {
128
+ const ruleId = "circuit.tool-timeout";
129
+ const evidence = [];
130
+ for (const event of events.filter(isToolEvent)) {
131
+ const duration = durationMs(event);
132
+ if (duration !== void 0 && duration > maxDurationMs) {
133
+ evidence.push({
134
+ ruleId,
135
+ toolName: toolLabel(event),
136
+ count: duration,
137
+ threshold: maxDurationMs,
138
+ eventId: event.eventId
139
+ });
140
+ }
141
+ }
142
+ if (evidence.length === 0) {
143
+ return closed(ruleId, "Tool durations within timeout.");
144
+ }
145
+ return open(ruleId, "Tool call exceeded configured timeout.", evidence, "warning");
146
+ }
147
+ function evaluateRunawayLlmLoop(events, maxLlmCalls) {
148
+ const ruleId = "circuit.runaway-llm-loop";
149
+ const llmCount = events.filter(isLlmEvent).length;
150
+ const hasTerminal = events.some((event) => {
151
+ const status = (event.status ?? event.attributes?.status ?? "").toString().toLowerCase();
152
+ return status === "ok" || status === "success" || status === "completed";
153
+ });
154
+ if (llmCount <= maxLlmCalls || hasTerminal) {
155
+ return closed(ruleId, "LLM call count within threshold or run completed.");
156
+ }
157
+ return open(ruleId, "Runaway LLM loop detected.", [
158
+ { ruleId, count: llmCount, threshold: maxLlmCalls }
159
+ ]);
160
+ }
161
+ function evaluateExcessiveBranchWidth(events, maxWidth) {
162
+ const ruleId = "circuit.excessive-branch-width";
163
+ const children = /* @__PURE__ */ new Map();
164
+ for (const event of events) {
165
+ const parentId = event.parentId;
166
+ if (!parentId) continue;
167
+ children.set(parentId, (children.get(parentId) ?? 0) + 1);
168
+ }
169
+ const evidence = [];
170
+ for (const [parentId, count] of children) {
171
+ if (count > maxWidth) {
172
+ evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });
173
+ }
174
+ }
175
+ if (evidence.length === 0) {
176
+ return closed(ruleId, "Branch width within threshold.");
177
+ }
178
+ return open(ruleId, "Excessive parallel branch width detected.", evidence, "warning");
179
+ }
180
+ function runRule(ruleId, events, options) {
181
+ switch (ruleId) {
182
+ case "circuit.same-tool-repetition":
183
+ if (options.sameToolRepetition === void 0) return void 0;
184
+ return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);
185
+ case "circuit.same-args-repetition":
186
+ if (options.sameArgsRepetition === void 0) return void 0;
187
+ return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);
188
+ case "circuit.max-loop-iterations":
189
+ if (options.maxLoopIterations === void 0) return void 0;
190
+ return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);
191
+ case "circuit.max-retries":
192
+ if (options.maxRetries === void 0) return void 0;
193
+ return evaluateMaxRetries(events, options.maxRetries.maxRetries);
194
+ case "circuit.tool-timeout":
195
+ if (options.toolTimeout === void 0) return void 0;
196
+ return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);
197
+ case "circuit.runaway-llm-loop":
198
+ if (options.runawayLlmLoop === void 0) return void 0;
199
+ return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);
200
+ case "circuit.excessive-branch-width":
201
+ if (options.excessiveBranchWidth === void 0) return void 0;
202
+ return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);
203
+ default:
204
+ return void 0;
205
+ }
206
+ }
207
+ function runCircuits(events, options = {}) {
208
+ const selected = options.rules ?? ALL_RULES;
209
+ const results = [];
210
+ for (const ruleId of selected) {
211
+ const result = runRule(ruleId, events, options);
212
+ if (result) results.push(result);
213
+ }
214
+ const ok = !results.some((result) => result.status === "open" && result.severity === "error");
215
+ return { ok, results };
216
+ }
217
+ var DEFAULT_REDACT_KEYS = [
218
+ "authorization",
219
+ "cookie",
220
+ "token",
221
+ "apiKey",
222
+ "password",
223
+ "secret",
224
+ "email"
225
+ ];
226
+ var SHARE_PROFILE_EXTRA_KEYS = [
227
+ "userEmail",
228
+ "customerEmail",
229
+ "phone",
230
+ "phoneNumber",
231
+ "address",
232
+ "ip",
233
+ "ipAddress",
234
+ "sessionId",
235
+ "requestId",
236
+ "correlationId",
237
+ "decisionId",
238
+ "groupId",
239
+ "customerId",
240
+ "userId",
241
+ "accountId",
242
+ "tenantId",
243
+ "orgId",
244
+ "organizationId",
245
+ "traceId",
246
+ "spanId",
247
+ "parentSpanId"
248
+ ];
249
+ var STRICT_PROFILE_EXTRA_KEYS = [
250
+ "prompt",
251
+ "completion",
252
+ "input",
253
+ "output",
254
+ "inputPreview",
255
+ "outputPreview",
256
+ "message",
257
+ "messages",
258
+ "transcript",
259
+ "context",
260
+ "document",
261
+ "documents",
262
+ "chunk",
263
+ "chunks",
264
+ "retrieval",
265
+ "query"
266
+ ];
267
+ function isRecord(value) {
268
+ return typeof value === "object" && value !== null && !Array.isArray(value);
269
+ }
270
+ function toKey(key) {
271
+ return key.toLowerCase();
272
+ }
273
+ function stableHash(value) {
274
+ const hash = crypto.createHash("sha256").update(value, "utf8").digest("hex");
275
+ return hash.slice(0, 8);
276
+ }
277
+ function stringifyScalar(value) {
278
+ if (typeof value === "string") return value;
279
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
280
+ return String(value);
281
+ }
282
+ return void 0;
283
+ }
284
+ function patternDetector(options) {
285
+ return {
286
+ id: options.id,
287
+ severity: options.severity ?? "warning",
288
+ matchKind: "value",
289
+ detect(input) {
290
+ if (typeof input.value !== "string") return [];
291
+ options.pattern.lastIndex = 0;
292
+ return options.pattern.test(input.value) ? [{ action: "replace", severity: options.severity ?? "warning", matchKind: "value" }] : [];
293
+ }
294
+ };
295
+ }
296
+ function digitsOnly(value) {
297
+ return value.replace(/\D/g, "");
298
+ }
299
+ function passesLuhn(value) {
300
+ const digits = digitsOnly(value);
301
+ if (digits.length < 13 || digits.length > 19) return false;
302
+ let sum = 0;
303
+ let double = false;
304
+ for (let i = digits.length - 1; i >= 0; i -= 1) {
305
+ let digit = Number(digits[i]);
306
+ if (double) {
307
+ digit *= 2;
308
+ if (digit > 9) digit -= 9;
309
+ }
310
+ sum += digit;
311
+ double = !double;
312
+ }
313
+ return sum % 10 === 0;
314
+ }
315
+ var credentialDetectors = [
316
+ patternDetector({
317
+ id: "value.authorizationHeader",
318
+ pattern: /^(?:basic|bearer|digest|apikey)\s+[a-z0-9._~+/=-]+$/i,
319
+ severity: "error"
320
+ }),
321
+ patternDetector({
322
+ id: "value.bearerToken",
323
+ pattern: /\bbearer\s+[a-z0-9._~+/=-]{12,}\b/i,
324
+ severity: "error"
325
+ }),
326
+ patternDetector({
327
+ id: "value.cookie",
328
+ pattern: /\b[a-z0-9_.-]+=[^;\s]+(?:;\s*[a-z0-9_.-]+=[^;\s]+)+/i,
329
+ severity: "error"
330
+ }),
331
+ patternDetector({
332
+ id: "value.jwt",
333
+ pattern: /\beyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\b/,
334
+ severity: "error"
335
+ }),
336
+ patternDetector({
337
+ id: "value.providerApiKey",
338
+ pattern: /\b(?:sk-(?:proj-)?[a-zA-Z0-9_-]{16,}|sk-ant-[a-zA-Z0-9_-]{16,}|AIza[0-9A-Za-z_-]{20,})\b/,
339
+ severity: "error"
340
+ }),
341
+ patternDetector({
342
+ id: "value.githubToken",
343
+ pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/,
344
+ severity: "error"
345
+ }),
346
+ patternDetector({
347
+ id: "value.awsAccessKey",
348
+ pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
349
+ severity: "error"
350
+ }),
351
+ patternDetector({
352
+ id: "value.privateKey",
353
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*-----END [A-Z ]*PRIVATE KEY-----/,
354
+ severity: "error"
355
+ }),
356
+ {
357
+ id: "value.creditCard",
358
+ severity: "error",
359
+ matchKind: "value",
360
+ detect(input) {
361
+ if (typeof input.value !== "string") return [];
362
+ const candidatePattern = /(?:\d[ -]?){13,19}/g;
363
+ for (const match of input.value.matchAll(candidatePattern)) {
364
+ const candidate = match[0] ?? "";
365
+ if (passesLuhn(candidate)) {
366
+ return [{ action: "replace", severity: "error", matchKind: "value" }];
367
+ }
368
+ }
369
+ return [];
370
+ }
371
+ }
372
+ ];
373
+ var identifierDetectors = [
374
+ patternDetector({
375
+ id: "value.email",
376
+ pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
377
+ }),
378
+ patternDetector({
379
+ id: "value.phone",
380
+ pattern: /\b(?:\+\d{1,3}[\s.-]?)?(?:\(?\d{3}\)?[\s.-])\d{3}[\s.-]\d{4}\b/
381
+ }),
382
+ patternDetector({
383
+ id: "value.ipv4",
384
+ pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/
385
+ }),
386
+ patternDetector({
387
+ id: "value.ipv6",
388
+ pattern: /\b(?:[0-9a-f]{1,4}:){2,7}[0-9a-f]{1,4}\b/i
389
+ })
390
+ ];
391
+ function builtInDetectorsForProfile(profile) {
392
+ if (profile === "local") return credentialDetectors;
393
+ return [...credentialDetectors, ...identifierDetectors];
394
+ }
395
+ function compileRules(rules, extraKeys) {
396
+ const out = /* @__PURE__ */ new Map();
397
+ const set = (rule) => {
398
+ const key = toKey(rule.key);
399
+ out.set(key, { ...rule, key });
400
+ };
401
+ for (const key of DEFAULT_REDACT_KEYS) {
402
+ set({ key, strategy: "full" });
403
+ }
404
+ for (const key of extraKeys ?? []) {
405
+ if (typeof key === "string" && key.length > 0) {
406
+ set({ key, strategy: "full" });
407
+ }
408
+ }
409
+ for (const rule of rules ?? []) {
410
+ if (typeof rule === "string") {
411
+ set({ key: rule, strategy: "full" });
412
+ continue;
413
+ }
414
+ if (rule.strategy === "full") set({ key: rule.key, strategy: "full" });
415
+ if (rule.strategy === "hash") set({ key: rule.key, strategy: "hash" });
416
+ if (rule.strategy === "prefix") {
417
+ set({
418
+ key: rule.key,
419
+ strategy: "prefix",
420
+ keep: typeof rule.keep === "number" ? rule.keep : 8
421
+ });
422
+ }
423
+ }
424
+ return [...out.values()];
425
+ }
426
+ function actionForRule(rule) {
427
+ if (rule.strategy === "full") return "replace";
428
+ return rule.strategy;
429
+ }
430
+ function applyRule(rule, value, replacement) {
431
+ if (rule.strategy === "full") return replacement;
432
+ const asString = stringifyScalar(value);
433
+ if (rule.strategy === "prefix") {
434
+ if (asString === void 0) return replacement;
435
+ const keep = Math.max(0, Math.floor(rule.keep));
436
+ return asString.length <= keep ? `${asString}\u2026` : `${asString.slice(0, keep)}\u2026`;
437
+ }
438
+ if (rule.strategy === "hash") {
439
+ if (asString === void 0) return "[HASH:unknown]";
440
+ return `[HASH:${stableHash(asString)}]`;
441
+ }
442
+ return value;
443
+ }
444
+ function childPath(path, key) {
445
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
446
+ return path ? `${path}.${key}` : key;
447
+ }
448
+ return `${path || "$"}[${JSON.stringify(key)}]`;
449
+ }
450
+ function indexPath(path, index) {
451
+ return `${path || "$"}[${index}]`;
452
+ }
453
+ function makeFinding(path, detector, action, matchKind, severity = "warning", preview) {
454
+ return preview === void 0 ? { path, detector, action, severity, matchKind } : { path, detector, action, severity, matchKind, preview };
455
+ }
456
+ function createRedactionProfile(profile = "local") {
457
+ switch (profile) {
458
+ case "local":
459
+ return { profile: "local", extraKeys: [] };
460
+ case "share":
461
+ return {
462
+ profile: "share",
463
+ extraKeys: SHARE_PROFILE_EXTRA_KEYS,
464
+ maxMetadataValueLengthCap: 500,
465
+ maxPreviewLengthCap: 200
466
+ };
467
+ case "strict":
468
+ return {
469
+ profile: "strict",
470
+ extraKeys: [...SHARE_PROFILE_EXTRA_KEYS, ...STRICT_PROFILE_EXTRA_KEYS],
471
+ maxMetadataValueLengthCap: 200,
472
+ maxPreviewLengthCap: 80
473
+ };
474
+ }
475
+ }
476
+ var Redactor = class {
477
+ #rules;
478
+ #detectors;
479
+ #profile;
480
+ #replacement;
481
+ #maxDepth;
482
+ #collectFindings;
483
+ constructor(options) {
484
+ const resolved = createRedactionProfile(options?.profile ?? "local");
485
+ this.#profile = resolved.profile;
486
+ this.#rules = compileRules(options?.rules, [
487
+ ...resolved.extraKeys,
488
+ ...options?.extraKeys ?? []
489
+ ]);
490
+ this.#detectors = [
491
+ ...builtInDetectorsForProfile(this.#profile),
492
+ ...options?.detectors ?? []
493
+ ];
494
+ this.#replacement = options?.replacement ?? "[REDACTED]";
495
+ this.#maxDepth = options?.maxDepth ?? 32;
496
+ this.#collectFindings = options?.collectFindings ?? true;
497
+ }
498
+ redactValue(key, value) {
499
+ return this.#redactValue(value, key, key, 0, {
500
+ findings: [],
501
+ seen: /* @__PURE__ */ new WeakMap()
502
+ });
503
+ }
504
+ redactRecord(record) {
505
+ return this.redact(record).value;
506
+ }
507
+ redact(value) {
508
+ const state = {
509
+ findings: [],
510
+ seen: /* @__PURE__ */ new WeakMap()
511
+ };
512
+ const redacted = this.#redactValue(value, void 0, "$", 0, state);
513
+ return {
514
+ value: redacted,
515
+ findings: state.findings,
516
+ redacted: state.findings.some((finding) => finding.action !== "keep"),
517
+ profile: this.#profile
518
+ };
519
+ }
520
+ #recordFinding(state, finding) {
521
+ if (this.#collectFindings) state.findings.push(finding);
522
+ }
523
+ #redactValue(value, key, path, depth, state) {
524
+ if (depth > this.#maxDepth) {
525
+ this.#recordFinding(
526
+ state,
527
+ makeFinding(path, "structure.maxDepth", "truncate", "value", "warning")
528
+ );
529
+ return "[Truncated]";
530
+ }
531
+ if (key !== void 0) {
532
+ const rule = this.#rules.find((candidate) => candidate.key === toKey(key));
533
+ if (rule) {
534
+ this.#recordFinding(
535
+ state,
536
+ makeFinding(path, `key.${rule.key}`, actionForRule(rule), "key", "warning")
537
+ );
538
+ return applyRule(rule, value, this.#replacement);
539
+ }
540
+ }
541
+ for (const detector of this.#detectors) {
542
+ const detections = detector.detect({ path, key, value });
543
+ for (const detection of detections) {
544
+ const action = detection.action ?? "replace";
545
+ this.#recordFinding(
546
+ state,
547
+ makeFinding(
548
+ path,
549
+ detector.id,
550
+ action,
551
+ detection.matchKind ?? detector.matchKind ?? "custom",
552
+ detection.severity ?? detector.severity ?? "warning",
553
+ detection.preview
554
+ )
555
+ );
556
+ if (action !== "keep") {
557
+ return detection.replacement ?? this.#replacement;
558
+ }
559
+ }
560
+ }
561
+ if (Array.isArray(value)) {
562
+ if (state.seen.has(value)) return state.seen.get(value);
563
+ const out = [];
564
+ state.seen.set(value, out);
565
+ value.forEach((item, index) => {
566
+ out[index] = this.#redactValue(item, void 0, indexPath(path, index), depth + 1, state);
567
+ });
568
+ return out;
569
+ }
570
+ if (isRecord(value)) {
571
+ if (state.seen.has(value)) return state.seen.get(value);
572
+ const out = {};
573
+ state.seen.set(value, out);
574
+ for (const [entryKey, entryValue] of Object.entries(value)) {
575
+ out[entryKey] = this.#redactValue(
576
+ entryValue,
577
+ entryKey,
578
+ childPath(path === "$" ? "" : path, entryKey),
579
+ depth + 1,
580
+ state
581
+ );
582
+ }
583
+ return out;
584
+ }
585
+ return value;
586
+ }
587
+ };
588
+ function createRedactor(options) {
589
+ return new Redactor(options);
590
+ }
591
+ function redact(value, options) {
592
+ return createRedactor(options).redact(value);
593
+ }
594
+
595
+ // packages/guardrails/src/rules.ts
596
+ var DEFAULT_INJECTION_PATTERNS = [
597
+ "ignore previous instructions",
598
+ "ignore all prior",
599
+ "disregard your instructions",
600
+ "system prompt",
601
+ "you are now",
602
+ "jailbreak"
603
+ ];
604
+ function pass(ruleId, message) {
605
+ return { ruleId, status: "pass", severity: "info", message, evidence: [] };
606
+ }
607
+ function fail(ruleId, message, evidence, severity = "error") {
608
+ return { ruleId, status: severity === "warning" ? "warn" : "fail", severity, message, evidence };
609
+ }
610
+ function boundedPreview(value, max = 80) {
611
+ if (value.length <= max) return value;
612
+ return `${value.slice(0, max - 3)}...`;
613
+ }
614
+ function evaluateBannedPhrase(text, options) {
615
+ const ruleId = "guardrail.banned-phrase";
616
+ const haystack = options.caseInsensitive !== false ? text.toLowerCase() : text;
617
+ const evidence = [];
618
+ for (const phrase of options.phrases) {
619
+ const needle = options.caseInsensitive !== false ? phrase.toLowerCase() : phrase;
620
+ if (needle.length > 0 && haystack.includes(needle)) {
621
+ evidence.push({ ruleId, match: phrase, preview: boundedPreview(text) });
622
+ }
623
+ }
624
+ if (evidence.length === 0) {
625
+ return pass(ruleId, "No banned phrases matched.");
626
+ }
627
+ return fail(ruleId, `Matched ${evidence.length} banned phrase(s).`, evidence);
628
+ }
629
+ var SEVERITY_RANK = { info: 0, warning: 1, error: 2 };
630
+ function evaluatePiiLeak(value, options = {}) {
631
+ const ruleId = "guardrail.pii-leak";
632
+ const minSeverity = options.minSeverity ?? "warning";
633
+ const result = redact(value, { profile: options.profile ?? "share", collectFindings: true });
634
+ const findings = result.findings.filter(
635
+ (finding) => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[minSeverity]
636
+ );
637
+ if (findings.length === 0) {
638
+ return pass(ruleId, "No PII-style redaction findings.");
639
+ }
640
+ const evidence = findings.map((finding) => ({
641
+ ruleId,
642
+ path: finding.path,
643
+ detector: finding.detector,
644
+ preview: finding.preview
645
+ }));
646
+ return fail(ruleId, `Detected ${findings.length} PII-style finding(s).`, evidence);
647
+ }
648
+ function measureDepth(value) {
649
+ if (value === null || typeof value !== "object") return 0;
650
+ if (Array.isArray(value)) {
651
+ return 1 + Math.max(0, ...value.map((item) => measureDepth(item)));
652
+ }
653
+ const depths = Object.values(value).map((item) => measureDepth(item));
654
+ return 1 + (depths.length === 0 ? 0 : Math.max(...depths));
655
+ }
656
+ function maxStringLength(value) {
657
+ if (typeof value === "string") return value.length;
658
+ if (value === null || typeof value !== "object") return 0;
659
+ if (Array.isArray(value)) {
660
+ return Math.max(0, ...value.map((item) => maxStringLength(item)));
661
+ }
662
+ return Math.max(0, ...Object.values(value).map((item) => maxStringLength(item)));
663
+ }
664
+ function evaluateUnsafeToolArgs(toolName, toolArgs2, options = {}) {
665
+ const ruleId = "guardrail.unsafe-tool-args";
666
+ const blocked = new Set((options.blockedTools ?? []).map((name) => name.toLowerCase()));
667
+ const evidence = [];
668
+ if (blocked.has(toolName.toLowerCase())) {
669
+ evidence.push({ ruleId, preview: toolName, match: toolName });
670
+ }
671
+ const maxDepth = options.maxDepth ?? 12;
672
+ const depth = measureDepth(toolArgs2);
673
+ if (depth > maxDepth) {
674
+ evidence.push({ ruleId, path: "args", preview: `depth=${depth}` });
675
+ }
676
+ const maxLen = options.maxStringLength ?? 16384;
677
+ const longest = maxStringLength(toolArgs2);
678
+ if (longest > maxLen) {
679
+ evidence.push({ ruleId, path: "args", preview: `maxStringLength=${longest}` });
680
+ }
681
+ if (evidence.length === 0) {
682
+ return pass(ruleId, "Tool arguments within configured bounds.");
683
+ }
684
+ return fail(ruleId, "Unsafe or oversized tool arguments detected.", evidence);
685
+ }
686
+ function evaluatePromptInjection(text, options = {}) {
687
+ const ruleId = "guardrail.prompt-injection";
688
+ const patterns = options.patterns ?? DEFAULT_INJECTION_PATTERNS;
689
+ const haystack = text.toLowerCase();
690
+ const evidence = [];
691
+ for (const pattern of patterns) {
692
+ const needle = pattern.toLowerCase();
693
+ if (needle.length > 0 && haystack.includes(needle)) {
694
+ evidence.push({ ruleId, match: pattern, preview: boundedPreview(text) });
695
+ }
696
+ }
697
+ if (evidence.length === 0) {
698
+ return pass(ruleId, "No prompt-injection patterns matched.");
699
+ }
700
+ return fail(ruleId, `Matched ${evidence.length} injection pattern(s).`, evidence, "warning");
701
+ }
702
+ function validateSchemaField(value, field, path, evidence) {
703
+ const ruleId = "guardrail.structured-output";
704
+ if (field.type) {
705
+ const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
706
+ if (actual !== field.type) {
707
+ evidence.push({ ruleId, path, preview: `expected ${field.type}, got ${actual}` });
708
+ return;
709
+ }
710
+ }
711
+ if (field.enum && !field.enum.some((item) => Object.is(item, value))) {
712
+ evidence.push({ ruleId, path, preview: "value not in enum" });
713
+ }
714
+ if (field.type === "object" && field.required && value && typeof value === "object" && !Array.isArray(value)) {
715
+ const record = value;
716
+ for (const key of field.required) {
717
+ if (!(key in record)) {
718
+ evidence.push({ ruleId, path: `${path}.${key}`, preview: "missing required key" });
719
+ }
720
+ }
721
+ }
722
+ }
723
+ function evaluateStructuredOutput(value, options) {
724
+ const ruleId = "guardrail.structured-output";
725
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
726
+ return fail(ruleId, "Structured output must be an object.", [
727
+ { ruleId, preview: typeof value }
728
+ ]);
729
+ }
730
+ const record = value;
731
+ const evidence = [];
732
+ for (const [key, field] of Object.entries(options.schema)) {
733
+ validateSchemaField(record[key], field, key, evidence);
734
+ }
735
+ if (evidence.length === 0) {
736
+ return pass(ruleId, "Structured output matches schema subset.");
737
+ }
738
+ return fail(ruleId, "Structured output schema violation.", evidence);
739
+ }
740
+ function evaluateOversizeOutput(value, options = {}) {
741
+ const ruleId = "guardrail.oversize-output";
742
+ const text = typeof value === "string" ? value : JSON.stringify(value);
743
+ const maxLength = options.maxLength ?? options.maxSerializedLength ?? 32768;
744
+ if (text.length <= maxLength) {
745
+ return pass(ruleId, "Output within size limits.");
746
+ }
747
+ return fail(ruleId, `Output exceeds max length (${text.length} > ${maxLength}).`, [
748
+ { ruleId, preview: `length=${text.length}` }
749
+ ]);
750
+ }
751
+ function evaluateRequiredJsonShape(value, options) {
752
+ const ruleId = "guardrail.required-json-shape";
753
+ let parsed = value;
754
+ if (typeof value === "string") {
755
+ try {
756
+ parsed = JSON.parse(value);
757
+ } catch {
758
+ return fail(ruleId, "Value is not valid JSON.", [{ ruleId, preview: boundedPreview(value) }]);
759
+ }
760
+ }
761
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
762
+ return fail(ruleId, "JSON value must be an object.", [{ ruleId, preview: typeof parsed }]);
763
+ }
764
+ const record = parsed;
765
+ const evidence = [];
766
+ for (const key of options.requiredKeys) {
767
+ if (!(key in record)) {
768
+ evidence.push({ ruleId, path: key, preview: "missing required key" });
769
+ }
770
+ }
771
+ if (evidence.length === 0) {
772
+ return pass(ruleId, "Required JSON keys present.");
773
+ }
774
+ return fail(ruleId, "Missing required JSON keys.", evidence);
775
+ }
776
+
777
+ // packages/guardrails/src/run.ts
778
+ var ALL_RULES2 = [
779
+ "guardrail.banned-phrase",
780
+ "guardrail.pii-leak",
781
+ "guardrail.unsafe-tool-args",
782
+ "guardrail.prompt-injection",
783
+ "guardrail.structured-output",
784
+ "guardrail.oversize-output",
785
+ "guardrail.required-json-shape"
786
+ ];
787
+ function isErrorFailure(result) {
788
+ return result.status === "fail" && result.severity === "error";
789
+ }
790
+ function runRule2(ruleId, input, options) {
791
+ switch (ruleId) {
792
+ case "guardrail.banned-phrase": {
793
+ if (!options.bannedPhrase || input.text === void 0) return void 0;
794
+ return evaluateBannedPhrase(input.text, options.bannedPhrase);
795
+ }
796
+ case "guardrail.pii-leak": {
797
+ if (input.value === void 0 && input.text === void 0) return void 0;
798
+ return evaluatePiiLeak(input.value ?? input.text, options.piiLeak);
799
+ }
800
+ case "guardrail.unsafe-tool-args": {
801
+ if (!input.toolName) return void 0;
802
+ return evaluateUnsafeToolArgs(input.toolName, input.toolArgs ?? {}, options.unsafeToolArgs);
803
+ }
804
+ case "guardrail.prompt-injection": {
805
+ if (input.text === void 0) return void 0;
806
+ return evaluatePromptInjection(input.text, options.promptInjection);
807
+ }
808
+ case "guardrail.structured-output": {
809
+ if (!options.structuredOutput || input.value === void 0) return void 0;
810
+ return evaluateStructuredOutput(input.value, options.structuredOutput);
811
+ }
812
+ case "guardrail.oversize-output": {
813
+ if (input.value === void 0 && input.text === void 0) return void 0;
814
+ return evaluateOversizeOutput(input.value ?? input.text, options.oversizeOutput);
815
+ }
816
+ case "guardrail.required-json-shape": {
817
+ if (!options.requiredJsonShape || input.value === void 0 && input.text === void 0) return void 0;
818
+ return evaluateRequiredJsonShape(input.value ?? input.text, options.requiredJsonShape);
819
+ }
820
+ default:
821
+ return void 0;
822
+ }
823
+ }
824
+ function runGuardrails(input, options = {}) {
825
+ const selected = options.rules ?? ALL_RULES2;
826
+ const results = [];
827
+ for (const ruleId of selected) {
828
+ const result = runRule2(ruleId, input, options);
829
+ if (result) results.push(result);
830
+ }
831
+ const ok = !results.some(isErrorFailure);
832
+ return { ok, results };
833
+ }
834
+
835
+ // packages/eval/src/safety-rules.ts
836
+ function pass2(ruleId, message) {
837
+ return { ruleId, status: "pass", severity: "info", message, evidence: [] };
838
+ }
839
+ function failFinding(ruleId, message, expected, actual) {
840
+ return {
841
+ ruleId,
842
+ status: "fail",
843
+ severity: "error",
844
+ message,
845
+ expected,
846
+ actual,
847
+ evidence: []
848
+ };
849
+ }
850
+ function warnFinding(ruleId, message, actual) {
851
+ return {
852
+ ruleId,
853
+ status: "warning",
854
+ severity: "warning",
855
+ message,
856
+ actual,
857
+ evidence: []
858
+ };
859
+ }
860
+ function collectAnswerText(context) {
861
+ const parts = [];
862
+ for (const node of context.nodes) {
863
+ const attrs = node.event.attributes ?? {};
864
+ for (const key of ["answer", "output", "text", "content"]) {
865
+ const value = attrs[key];
866
+ if (typeof value === "string") parts.push(value);
867
+ }
868
+ }
869
+ return parts.join("\n");
870
+ }
871
+ function createEvalGuardrailRule(options) {
872
+ return {
873
+ id: "eval.guardrails",
874
+ category: "safety",
875
+ evaluate(context) {
876
+ const text = collectAnswerText(context);
877
+ const run = runGuardrails({ text, value: context.events }, options);
878
+ return run.results.flatMap((result) => {
879
+ if (result.status === "pass") return [pass2(result.ruleId, result.message)];
880
+ if (result.status === "warn") return [warnFinding(result.ruleId, result.message)];
881
+ return [failFinding(result.ruleId, result.message)];
882
+ });
883
+ }
884
+ };
885
+ }
886
+ function createEvalCircuitRule(options) {
887
+ return {
888
+ id: "eval.circuits",
889
+ category: "structure",
890
+ evaluate(context) {
891
+ const events = context.events.map((event) => ({
892
+ eventId: event.eventId,
893
+ runId: event.runId,
894
+ name: event.name,
895
+ kind: event.kind,
896
+ parentId: event.parentId,
897
+ startedAt: event.startedAt,
898
+ endedAt: event.endedAt,
899
+ durationMs: event.durationMs,
900
+ attributes: event.attributes,
901
+ status: event.status
902
+ }));
903
+ const run = runCircuits(events, options);
904
+ return run.results.flatMap((result) => {
905
+ if (result.status === "closed") return [pass2(result.ruleId, result.message)];
906
+ if (result.status === "warn") return [warnFinding(result.ruleId, result.message, result.evidence[0]?.count)];
907
+ return [failFinding(result.ruleId, result.message, result.evidence[0]?.threshold, result.evidence[0]?.count)];
908
+ });
909
+ }
910
+ };
911
+ }
2
912
 
3
913
  // packages/eval/src/index.ts
4
914
  function isTraceReadResult(value) {
@@ -183,7 +1093,7 @@ function evidenceForEvent(event, path) {
183
1093
  }
184
1094
  ];
185
1095
  }
186
- function fail(ruleId, message, evidence, expected, actual) {
1096
+ function fail2(ruleId, message, evidence, expected, actual) {
187
1097
  return {
188
1098
  ruleId,
189
1099
  status: "fail",
@@ -379,7 +1289,7 @@ var checks = {
379
1289
  "eval.requireSuccess",
380
1290
  "run",
381
1291
  (context) => context.run.status === "ok" ? [] : [
382
- fail(
1292
+ fail2(
383
1293
  "eval.requireSuccess",
384
1294
  "Run did not complete successfully.",
385
1295
  evidenceForRun(context.run, "status"),
@@ -394,7 +1304,7 @@ var checks = {
394
1304
  return createRule("eval.requiredTools", "tool", (context) => {
395
1305
  const tools = new Set(nodeNames(context.nodes, "TOOL"));
396
1306
  return expected.filter((name) => !tools.has(name)).map(
397
- (name) => fail(
1307
+ (name) => fail2(
398
1308
  "eval.requiredTools",
399
1309
  `Required tool ${name} did not appear.`,
400
1310
  evidenceForRun(context.run, "children"),
@@ -410,7 +1320,7 @@ var checks = {
410
1320
  "eval.forbiddenTools",
411
1321
  "tool",
412
1322
  (context) => context.nodes.filter((node) => node.event.kind === "TOOL" && blocked.includes(node.event.name)).map(
413
- (node) => fail(
1323
+ (node) => fail2(
414
1324
  "eval.forbiddenTools",
415
1325
  `Forbidden tool ${node.event.name} appeared.`,
416
1326
  evidenceForEvent(node.event, "name"),
@@ -425,7 +1335,7 @@ var checks = {
425
1335
  "eval.maxDurationMs",
426
1336
  "run",
427
1337
  (context) => context.run.durationMs !== void 0 && context.run.durationMs > maxDurationMs ? [
428
- fail(
1338
+ fail2(
429
1339
  "eval.maxDurationMs",
430
1340
  `Run duration exceeded ${maxDurationMs}ms.`,
431
1341
  evidenceForRun(context.run, "durationMs"),
@@ -442,7 +1352,7 @@ var checks = {
442
1352
  void 0
443
1353
  );
444
1354
  return deepest !== void 0 && deepest.depth > maxDepth ? [
445
- fail(
1355
+ fail2(
446
1356
  "eval.maxDepth",
447
1357
  `Run tree depth exceeded ${maxDepth}.`,
448
1358
  evidenceForEvent(deepest.event, "depth"),
@@ -459,7 +1369,7 @@ var checks = {
459
1369
  (context) => context.nodes.flatMap((node) => {
460
1370
  const retries = numericAttribute(node, ["retryCount", "retries", "attempt"]);
461
1371
  return retries !== void 0 && retries > maxRetries ? [
462
- fail(
1372
+ fail2(
463
1373
  "eval.maxRetries",
464
1374
  `Retry count exceeded ${maxRetries}.`,
465
1375
  evidenceForEvent(node.event, "attributes.retryCount"),
@@ -474,7 +1384,7 @@ var checks = {
474
1384
  return createRule("eval.maxTotalTokens", "llm", (context) => {
475
1385
  const total = totalTokenCount(context.events);
476
1386
  return total > maxTotalTokens ? [
477
- fail(
1387
+ fail2(
478
1388
  "eval.maxTotalTokens",
479
1389
  `Total token usage exceeded ${maxTotalTokens}.`,
480
1390
  evidenceForRun(context.run, "tokenUsage.total"),
@@ -489,7 +1399,7 @@ var checks = {
489
1399
  "eval.noFailedSteps",
490
1400
  "run",
491
1401
  (context) => context.nodes.filter((node) => node.event.status === "error" || node.event.kind === "ERROR").map(
492
- (node) => fail(
1402
+ (node) => fail2(
493
1403
  "eval.noFailedSteps",
494
1404
  "Run contains a failed step or error node.",
495
1405
  evidenceForEvent(node.event, "status"),
@@ -507,7 +1417,7 @@ var checks = {
507
1417
  (node, index) => index < firstLlmIndex && node.event.kind === "RETRIEVER"
508
1418
  );
509
1419
  return retrievalIndex === -1 ? [
510
- fail(
1420
+ fail2(
511
1421
  "eval.requiredRetrievalBeforeGeneration",
512
1422
  "No retrieval step appeared before the first LLM generation.",
513
1423
  evidenceForEvent(context.nodes[firstLlmIndex].event, "kind"),
@@ -523,7 +1433,7 @@ var checks = {
523
1433
  const decisions = context.nodes.filter((node) => node.event.kind === "DECISION");
524
1434
  if (decisions.length === 0) {
525
1435
  return [
526
- fail(
1436
+ fail2(
527
1437
  "eval.requiredDecisionMetadata",
528
1438
  "No decision node is available for required metadata.",
529
1439
  evidenceForRun(context.run, "children"),
@@ -534,7 +1444,7 @@ var checks = {
534
1444
  }
535
1445
  return decisions.flatMap(
536
1446
  (node) => required.filter((key) => !hasAttribute(node, key)).map(
537
- (key) => fail(
1447
+ (key) => fail2(
538
1448
  "eval.requiredDecisionMetadata",
539
1449
  `Decision metadata ${key} is missing.`,
540
1450
  evidenceForEvent(node.event, `attributes.${key}`),
@@ -555,7 +1465,7 @@ var checks = {
555
1465
  const contexts = collectTextFields(context.nodes, contextKeys, ["RETRIEVER", "TOOL"]);
556
1466
  if (answers.length === 0 || contexts.length === 0) {
557
1467
  return [
558
- fail(
1468
+ fail2(
559
1469
  "eval.contextOverlap",
560
1470
  "Answer and context text are required for overlap evaluation.",
561
1471
  firstEvidence(answers.length > 0 ? answers : contexts, context.run, "children"),
@@ -569,7 +1479,7 @@ var checks = {
569
1479
  const sharedTerms = [...answerTerms].filter((term) => contextTerms.has(term)).length;
570
1480
  const overlap = answerTerms.size === 0 ? 0 : sharedTerms / answerTerms.size;
571
1481
  return sharedTerms < minSharedTerms || overlap < minOverlap ? [
572
- fail(
1482
+ fail2(
573
1483
  "eval.contextOverlap",
574
1484
  "Answer text did not sufficiently overlap retrieved context.",
575
1485
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -597,7 +1507,7 @@ var checks = {
597
1507
  const quotes = quotedSnippets(answerText, minQuoteLength);
598
1508
  if (quotes.length === 0) {
599
1509
  return requireQuote ? [
600
- fail(
1510
+ fail2(
601
1511
  "eval.quoteOverlap",
602
1512
  "Answer did not contain a quote for overlap evaluation.",
603
1513
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -608,7 +1518,7 @@ var checks = {
608
1518
  }
609
1519
  const missing = quotes.filter((quote) => !contextText.includes(quote.toLowerCase()));
610
1520
  return missing.length > 0 ? [
611
- fail(
1521
+ fail2(
612
1522
  "eval.quoteOverlap",
613
1523
  "Quoted answer text did not appear in retrieved context.",
614
1524
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -626,7 +1536,7 @@ var checks = {
626
1536
  const citations = collectTextFields(context.nodes, citationKeys);
627
1537
  const count = citationCount(answers.map((field) => field.text).join(" "), citations);
628
1538
  return count === 0 ? [
629
- fail(
1539
+ fail2(
630
1540
  "eval.citationPresence",
631
1541
  "Answer did not include citations or source references.",
632
1542
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -644,7 +1554,7 @@ var checks = {
644
1554
  const availableSet = new Set(available);
645
1555
  const missing = expected.filter((id) => !availableSet.has(id));
646
1556
  return missing.length > 0 ? [
647
- fail(
1557
+ fail2(
648
1558
  "eval.requiredSourceIds",
649
1559
  "Required source IDs were not present in trace context or citations.",
650
1560
  evidenceForRun(context.run, "children"),
@@ -664,7 +1574,7 @@ var checks = {
664
1574
  const tooShort = options.minCharacters !== void 0 && characters < options.minCharacters || options.minWords !== void 0 && words < options.minWords;
665
1575
  const tooLong = options.maxCharacters !== void 0 && characters > options.maxCharacters || options.maxWords !== void 0 && words > options.maxWords;
666
1576
  return answer.length === 0 || tooShort || tooLong ? [
667
- fail(
1577
+ fail2(
668
1578
  "eval.answerLengthBounds",
669
1579
  "Answer length fell outside required bounds.",
670
1580
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -687,7 +1597,7 @@ var checks = {
687
1597
  const answer = answers.map((field) => field.text).join(" ").toLowerCase();
688
1598
  const matches = banned.filter((phrase) => answer.includes(phrase));
689
1599
  return matches.length > 0 ? [
690
- fail(
1600
+ fail2(
691
1601
  "eval.bannedUnsupportedPhrases",
692
1602
  "Answer contained banned unsupported-answer phrasing.",
693
1603
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -724,6 +1634,6 @@ function renderEvalMarkdown(result) {
724
1634
  `;
725
1635
  }
726
1636
 
727
- export { checks, evalRun, renderEvalMarkdown };
1637
+ export { checks, createEvalCircuitRule, createEvalGuardrailRule, evalRun, renderEvalMarkdown };
728
1638
  //# sourceMappingURL=index.mjs.map
729
1639
  //# sourceMappingURL=index.mjs.map