agent-inspect 5.0.0 → 5.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.
@@ -2100,7 +2100,7 @@ function buildRunSummary(events) {
2100
2100
  }
2101
2101
  let totalSteps = 0;
2102
2102
  let llmSteps = 0;
2103
- let toolSteps = 0;
2103
+ let toolSteps2 = 0;
2104
2104
  let logicSteps = 0;
2105
2105
  let errorSteps = 0;
2106
2106
  let maxDepth = 0;
@@ -2130,7 +2130,7 @@ function buildRunSummary(events) {
2130
2130
  for (const [id, s] of steps.entries()) {
2131
2131
  totalSteps += 1;
2132
2132
  if (s.type === "llm") llmSteps += 1;
2133
- else if (s.type === "tool") toolSteps += 1;
2133
+ else if (s.type === "tool") toolSteps2 += 1;
2134
2134
  else logicSteps += 1;
2135
2135
  if (s.status === "error") errorSteps += 1;
2136
2136
  const depth = computeDepth(id);
@@ -2164,7 +2164,7 @@ function buildRunSummary(events) {
2164
2164
  durationMs: durationMs2,
2165
2165
  totalSteps,
2166
2166
  llmSteps,
2167
- toolSteps,
2167
+ toolSteps: toolSteps2,
2168
2168
  logicSteps,
2169
2169
  errorSteps,
2170
2170
  maxDepth,
@@ -3786,12 +3786,12 @@ function buildCriticalPath(runs, handoffs) {
3786
3786
  handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.from)
3787
3787
  );
3788
3788
  const ordered = [...runs].sort(compareRuns);
3789
- const path30 = [];
3789
+ const path31 = [];
3790
3790
  const visited = /* @__PURE__ */ new Set();
3791
3791
  const pushRun = (run, confidence, source) => {
3792
3792
  if (visited.has(run.runId)) return;
3793
3793
  visited.add(run.runId);
3794
- path30.push({
3794
+ path31.push({
3795
3795
  runId: run.runId,
3796
3796
  name: run.name,
3797
3797
  startedAt: run.startedAt,
@@ -3816,7 +3816,7 @@ function buildCriticalPath(runs, handoffs) {
3816
3816
  const confidence = explicitTargets.has(run.runId) || explicitSources.has(run.runId) ? "explicit" : "correlated";
3817
3817
  pushRun(run, confidence, confidence === "explicit" ? "manual" : "inferred");
3818
3818
  }
3819
- return path30;
3819
+ return path31;
3820
3820
  }
3821
3821
  function metaRunIdMatches(run, token, runById) {
3822
3822
  const meta2 = extractSessionWorkflowMetadata(run.metadata);
@@ -4716,7 +4716,7 @@ function stripPrefix(name, prefixes) {
4716
4716
  }
4717
4717
  return name;
4718
4718
  }
4719
- function eventEvidence(event, path30) {
4719
+ function eventEvidence(event, path31) {
4720
4720
  return {
4721
4721
  runId: event.runId,
4722
4722
  eventId: event.eventId,
@@ -4726,7 +4726,7 @@ function eventEvidence(event, path30) {
4726
4726
  kind: event.kind,
4727
4727
  name: event.name,
4728
4728
  status: event.status,
4729
- ...path30 ? { path: path30 } : {}
4729
+ ...path31 ? { path: path31 } : {}
4730
4730
  };
4731
4731
  }
4732
4732
  function runEvidence(run) {
@@ -4789,9 +4789,9 @@ function eventEndMs(event) {
4789
4789
  function normalizedKey(value) {
4790
4790
  return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
4791
4791
  }
4792
- function lastPathSegment(path30) {
4793
- const parts = path30.split(".");
4794
- return parts[parts.length - 1] ?? path30;
4792
+ function lastPathSegment(path31) {
4793
+ const parts = path31.split(".");
4794
+ return parts[parts.length - 1] ?? path31;
4795
4795
  }
4796
4796
  function valueType(value) {
4797
4797
  if (Array.isArray(value)) return "array";
@@ -4805,12 +4805,12 @@ function serializedByteLength(value) {
4805
4805
  return void 0;
4806
4806
  }
4807
4807
  }
4808
- function pushValueEntries(entries, event, value, path30, key, depth = 0) {
4809
- entries.push({ event, path: path30, key, value });
4808
+ function pushValueEntries(entries, event, value, path31, key, depth = 0) {
4809
+ entries.push({ event, path: path31, key, value });
4810
4810
  if (depth >= 8) return;
4811
4811
  if (Array.isArray(value)) {
4812
4812
  for (const [index, item] of value.entries()) {
4813
- pushValueEntries(entries, event, item, `${path30}.${index}`, String(index), depth + 1);
4813
+ pushValueEntries(entries, event, item, `${path31}.${index}`, String(index), depth + 1);
4814
4814
  }
4815
4815
  return;
4816
4816
  }
@@ -4820,7 +4820,7 @@ function pushValueEntries(entries, event, value, path30, key, depth = 0) {
4820
4820
  entries,
4821
4821
  event,
4822
4822
  value[nestedKey],
4823
- `${path30}.${nestedKey}`,
4823
+ `${path31}.${nestedKey}`,
4824
4824
  nestedKey,
4825
4825
  depth + 1
4826
4826
  );
@@ -4901,9 +4901,9 @@ function eventDurationMs(event) {
4901
4901
  }
4902
4902
  function treeShape(nodes) {
4903
4903
  const lines = [];
4904
- const visit = (node, path30) => {
4905
- lines.push(`${path30}:${node.event.kind}:${node.event.name}:${node.event.status ?? "unknown"}`);
4906
- node.children.forEach((child, index) => visit(child, `${path30}.${index}`));
4904
+ const visit = (node, path31) => {
4905
+ lines.push(`${path31}:${node.event.kind}:${node.event.name}:${node.event.status ?? "unknown"}`);
4906
+ node.children.forEach((child, index) => visit(child, `${path31}.${index}`));
4907
4907
  };
4908
4908
  nodes.forEach((node, index) => visit(node, String(index)));
4909
4909
  return lines;
@@ -4952,9 +4952,9 @@ function retrievalShape(context) {
4952
4952
  function guardrailShape(context) {
4953
4953
  return guardrailEvents(context).map((event) => signalName(event, ["guardrailName", "guardrail", "guardrailId"], ["guardrail:"])).sort((a, b) => a.localeCompare(b));
4954
4954
  }
4955
- function firstEvidenceForKind(context, kind, path30) {
4955
+ function firstEvidenceForKind(context, kind, path31) {
4956
4956
  const event = context.events.find((candidate) => candidate.kind === kind);
4957
- return event ? [eventEvidence(event, path30)] : runEvidence(context.selectedRun);
4957
+ return event ? [eventEvidence(event, path31)] : runEvidence(context.selectedRun);
4958
4958
  }
4959
4959
  function baselineDiffFinding(message, evidence, expected, actual) {
4960
4960
  return failFinding("baseline.regression", message, evidence, expected, actual);
@@ -5304,13 +5304,13 @@ function createStructureCycleRule() {
5304
5304
  const seenCycles = /* @__PURE__ */ new Set();
5305
5305
  const findings = [];
5306
5306
  for (const event of [...context.events].sort((a, b) => a.eventId.localeCompare(b.eventId))) {
5307
- const path30 = [];
5307
+ const path31 = [];
5308
5308
  const seenAt = /* @__PURE__ */ new Map();
5309
5309
  let current = event;
5310
5310
  while (current) {
5311
5311
  const existing = seenAt.get(current.eventId);
5312
5312
  if (existing !== void 0) {
5313
- const cycle = path30.slice(existing);
5313
+ const cycle = path31.slice(existing);
5314
5314
  const key = cycle.map((item) => item.eventId).sort().join("\0");
5315
5315
  if (!seenCycles.has(key)) {
5316
5316
  seenCycles.add(key);
@@ -5326,8 +5326,8 @@ function createStructureCycleRule() {
5326
5326
  }
5327
5327
  break;
5328
5328
  }
5329
- seenAt.set(current.eventId, path30.length);
5330
- path30.push(current);
5329
+ seenAt.set(current.eventId, path31.length);
5330
+ path31.push(current);
5331
5331
  current = current.parentId ? byId.get(current.parentId) : void 0;
5332
5332
  }
5333
5333
  }
@@ -8275,6 +8275,682 @@ var init_suite = __esm({
8275
8275
  }
8276
8276
  });
8277
8277
 
8278
+ // packages/core/src/cohort/types.ts
8279
+ var COHORT_METRIC_IDS;
8280
+ var init_types5 = __esm({
8281
+ "packages/core/src/cohort/types.ts"() {
8282
+ COHORT_METRIC_IDS = [
8283
+ "errorRate",
8284
+ "duration",
8285
+ "toolChoice",
8286
+ "toolOrdering",
8287
+ "llmCallCount",
8288
+ "tokenUsage",
8289
+ "retryCount",
8290
+ "observationFailure",
8291
+ "guardrailFailure",
8292
+ "circuitViolation",
8293
+ "redactionWarning"
8294
+ ];
8295
+ }
8296
+ });
8297
+
8298
+ // packages/core/src/cohort/compare.ts
8299
+ function compareNumber(metric, label, baseline, candidate, higherIsWorse = true) {
8300
+ if (baseline === void 0 && candidate === void 0) return void 0;
8301
+ const delta = baseline !== void 0 && candidate !== void 0 ? candidate - baseline : void 0;
8302
+ const regression = delta !== void 0 && (higherIsWorse && delta > 0 || !higherIsWorse && delta < 0);
8303
+ return {
8304
+ metric,
8305
+ baseline,
8306
+ candidate,
8307
+ delta,
8308
+ regression,
8309
+ message: `${label}: baseline=${baseline ?? "n/a"} candidate=${candidate ?? "n/a"}${delta !== void 0 ? ` (delta ${delta})` : ""}`
8310
+ };
8311
+ }
8312
+ function pickAggregate(groups, cohortLabel, groupKey) {
8313
+ return groups.find(
8314
+ (group) => group.cohortLabel === cohortLabel && (groupKey === void 0 || group.groupKey === groupKey)
8315
+ );
8316
+ }
8317
+ function compareCohortAggregates(groups, options) {
8318
+ const baselineAgg = pickAggregate(groups, options.baseline, options.groupKey);
8319
+ const candidateAgg = pickAggregate(groups, options.candidate, options.groupKey);
8320
+ const comparisons = [];
8321
+ for (const metric of options.metrics) {
8322
+ switch (metric) {
8323
+ case "errorRate": {
8324
+ const item = compareNumber(
8325
+ metric,
8326
+ "Error rate",
8327
+ baselineAgg?.errorRate,
8328
+ candidateAgg?.errorRate
8329
+ );
8330
+ if (item) comparisons.push(item);
8331
+ break;
8332
+ }
8333
+ case "duration": {
8334
+ const item = compareNumber(
8335
+ metric,
8336
+ "Average duration (ms)",
8337
+ baselineAgg?.avgDurationMs,
8338
+ candidateAgg?.avgDurationMs
8339
+ );
8340
+ if (item) comparisons.push(item);
8341
+ break;
8342
+ }
8343
+ case "llmCallCount": {
8344
+ const item = compareNumber(
8345
+ metric,
8346
+ "Average LLM calls",
8347
+ baselineAgg?.avgLlmCallCount,
8348
+ candidateAgg?.avgLlmCallCount
8349
+ );
8350
+ if (item) comparisons.push(item);
8351
+ break;
8352
+ }
8353
+ case "tokenUsage": {
8354
+ const item = compareNumber(
8355
+ metric,
8356
+ "Average token usage",
8357
+ baselineAgg?.avgTokenUsage,
8358
+ candidateAgg?.avgTokenUsage
8359
+ );
8360
+ if (item) comparisons.push(item);
8361
+ break;
8362
+ }
8363
+ case "retryCount": {
8364
+ const item = compareNumber(
8365
+ metric,
8366
+ "Average retries",
8367
+ baselineAgg?.avgRetryCount,
8368
+ candidateAgg?.avgRetryCount
8369
+ );
8370
+ if (item) comparisons.push(item);
8371
+ break;
8372
+ }
8373
+ case "observationFailure": {
8374
+ const item = compareNumber(
8375
+ metric,
8376
+ "Observation failure rate",
8377
+ baselineAgg?.observationFailureRate,
8378
+ candidateAgg?.observationFailureRate
8379
+ );
8380
+ if (item) comparisons.push(item);
8381
+ break;
8382
+ }
8383
+ case "toolChoice": {
8384
+ const baselineValue = baselineAgg?.dominantToolChoice;
8385
+ const candidateValue = candidateAgg?.dominantToolChoice;
8386
+ comparisons.push({
8387
+ metric,
8388
+ baseline: baselineValue,
8389
+ candidate: candidateValue,
8390
+ delta: baselineValue === candidateValue ? "same" : "changed",
8391
+ regression: baselineValue !== candidateValue,
8392
+ message: `Tool choice: baseline=${baselineValue ?? "n/a"} candidate=${candidateValue ?? "n/a"}`
8393
+ });
8394
+ break;
8395
+ }
8396
+ case "toolOrdering": {
8397
+ const baselineValue = baselineAgg?.toolOrderingSignature;
8398
+ const candidateValue = candidateAgg?.toolOrderingSignature;
8399
+ comparisons.push({
8400
+ metric,
8401
+ baseline: baselineValue,
8402
+ candidate: candidateValue,
8403
+ delta: baselineValue === candidateValue ? "same" : "changed",
8404
+ regression: baselineValue !== candidateValue,
8405
+ message: `Tool ordering: baseline=${baselineValue ?? "n/a"} candidate=${candidateValue ?? "n/a"}`
8406
+ });
8407
+ break;
8408
+ }
8409
+ case "guardrailFailure": {
8410
+ const item = compareNumber(
8411
+ metric,
8412
+ "Guardrail failures",
8413
+ baselineAgg?.avgGuardrailFailures,
8414
+ candidateAgg?.avgGuardrailFailures
8415
+ );
8416
+ if (item) comparisons.push(item);
8417
+ break;
8418
+ }
8419
+ case "circuitViolation": {
8420
+ const item = compareNumber(
8421
+ metric,
8422
+ "Circuit violations",
8423
+ baselineAgg?.avgCircuitViolations,
8424
+ candidateAgg?.avgCircuitViolations
8425
+ );
8426
+ if (item) comparisons.push(item);
8427
+ break;
8428
+ }
8429
+ case "redactionWarning": {
8430
+ const item = compareNumber(
8431
+ metric,
8432
+ "Redaction warnings",
8433
+ baselineAgg?.avgRedactionWarnings,
8434
+ candidateAgg?.avgRedactionWarnings
8435
+ );
8436
+ if (item) comparisons.push(item);
8437
+ break;
8438
+ }
8439
+ }
8440
+ }
8441
+ return comparisons;
8442
+ }
8443
+ var init_compare = __esm({
8444
+ "packages/core/src/cohort/compare.ts"() {
8445
+ }
8446
+ });
8447
+
8448
+ // packages/core/src/cohort/grouping.ts
8449
+ function parseCohortMetricList(value) {
8450
+ if (value === void 0 || value.trim() === "") return [];
8451
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
8452
+ }
8453
+ function parseGroupBySpec(groupBy) {
8454
+ const raw = (groupBy ?? "model").trim();
8455
+ if (raw === "model") return { kind: "model" };
8456
+ if (raw === "session") return { kind: "session" };
8457
+ if (raw === "group") return { kind: "group" };
8458
+ if (raw.startsWith("metadata.")) {
8459
+ const metadataKey = raw.slice("metadata.".length).trim();
8460
+ if (metadataKey === "") throw new Error("metadata group-by requires a key.");
8461
+ return { kind: "metadata", metadataKey };
8462
+ }
8463
+ throw new Error(`Unsupported --group-by value: ${raw}`);
8464
+ }
8465
+ function metadataString(metadata, key) {
8466
+ const value = metadata?.[key];
8467
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
8468
+ }
8469
+ function resolveRunGroupKey(run, groupBy) {
8470
+ const metadata = run.metadata ?? {};
8471
+ switch (groupBy.kind) {
8472
+ case "model":
8473
+ return metadataString(metadata, "model") ?? "unknown";
8474
+ case "session":
8475
+ return extractSessionWorkflowMetadata(metadata)?.sessionId ?? metadataString(metadata, "sessionId") ?? "__unscoped__";
8476
+ case "group":
8477
+ return extractSessionWorkflowMetadata(metadata)?.groupId ?? metadataString(metadata, "groupId") ?? "__unscoped__";
8478
+ case "metadata":
8479
+ return metadataString(metadata, groupBy.metadataKey) ?? "__missing__";
8480
+ default:
8481
+ return "unknown";
8482
+ }
8483
+ }
8484
+ function resolveCohortLabel(run, cohortKey, baseline, candidate) {
8485
+ const label = metadataString(run.metadata, cohortKey);
8486
+ if (label === void 0) return void 0;
8487
+ if (baseline !== void 0 && label === baseline) return baseline;
8488
+ if (candidate !== void 0 && label === candidate) return candidate;
8489
+ if (baseline === void 0 && candidate === void 0) return label;
8490
+ return void 0;
8491
+ }
8492
+ function filterRunsForCohort(runs, options) {
8493
+ const warnings = [];
8494
+ if (options.baseline === void 0 && options.candidate === void 0) {
8495
+ return { runs: [...runs], warnings };
8496
+ }
8497
+ const selected = [];
8498
+ for (const run of runs) {
8499
+ const label = resolveCohortLabel(
8500
+ run,
8501
+ options.cohortKey,
8502
+ options.baseline,
8503
+ options.candidate
8504
+ );
8505
+ if (label !== void 0) selected.push(run);
8506
+ }
8507
+ if (selected.length === 0) {
8508
+ warnings.push(
8509
+ `No runs matched baseline/candidate labels on metadata.${options.cohortKey}.`
8510
+ );
8511
+ }
8512
+ return { runs: selected, warnings };
8513
+ }
8514
+ var init_grouping = __esm({
8515
+ "packages/core/src/cohort/grouping.ts"() {
8516
+ init_metadata();
8517
+ }
8518
+ });
8519
+
8520
+ // packages/core/src/cohort/metrics.ts
8521
+ function asNumber(value) {
8522
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8523
+ }
8524
+ function toolSteps(events) {
8525
+ const ordering = [];
8526
+ const choices = /* @__PURE__ */ new Set();
8527
+ const sorted = [...events].filter((event) => event.event === "step_started").sort((a, b) => a.timestamp - b.timestamp);
8528
+ for (const event of sorted) {
8529
+ const step = event;
8530
+ if (step.type !== "tool") continue;
8531
+ const name = typeof step.metadata?.toolName === "string" ? step.metadata.toolName : step.name;
8532
+ ordering.push(name);
8533
+ choices.add(name);
8534
+ }
8535
+ return { choices: [...choices].sort(), ordering };
8536
+ }
8537
+ async function computeCohortRunMetrics(input3) {
8538
+ const events = await readTraceEventsFromFile(input3.filePath);
8539
+ const summary = buildRunSummary(events);
8540
+ const tools = toolSteps(events);
8541
+ const outcomes = extractOutcomesFromTraceEvents(events);
8542
+ const observationFailures = outcomes.filter((item) => item.status === "failed").length;
8543
+ const metadata = input3.metadata ?? {};
8544
+ const retryCount2 = asNumber(metadata.attempt) !== void 0 && asNumber(metadata.attempt) > 1 ? asNumber(metadata.attempt) - 1 : typeof metadata.retryOf === "string" ? 1 : 0;
8545
+ return {
8546
+ runId: input3.runId,
8547
+ ...input3.cohortLabel !== void 0 ? { cohortLabel: input3.cohortLabel } : {},
8548
+ groupKey: input3.groupKey,
8549
+ status: summary.status,
8550
+ error: summary.status === "error",
8551
+ durationMs: summary.durationMs ?? input3.durationMs,
8552
+ llmCallCount: summary.llmSteps,
8553
+ tokenUsageTotal: summary.totalTokens?.total,
8554
+ retryCount: retryCount2,
8555
+ observationFailures,
8556
+ guardrailFailures: asNumber(metadata.guardrailFailures) ?? 0,
8557
+ circuitViolations: asNumber(metadata.circuitViolations) ?? 0,
8558
+ redactionWarnings: asNumber(metadata.redactionWarnings) ?? 0,
8559
+ toolChoices: tools.choices,
8560
+ toolOrdering: tools.ordering
8561
+ };
8562
+ }
8563
+ function percentile2(values, p) {
8564
+ if (values.length === 0) return void 0;
8565
+ const sorted = [...values].sort((a, b) => a - b);
8566
+ const idx = Math.min(
8567
+ sorted.length - 1,
8568
+ Math.max(0, Math.ceil(p / 100 * sorted.length) - 1)
8569
+ );
8570
+ return sorted[idx];
8571
+ }
8572
+ function dominantToolChoice(runs) {
8573
+ const counts = /* @__PURE__ */ new Map();
8574
+ for (const run of runs) {
8575
+ const signature = run.toolChoices.join(",");
8576
+ if (signature === "") continue;
8577
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
8578
+ }
8579
+ let best;
8580
+ let bestCount = 0;
8581
+ for (const [key, count] of counts) {
8582
+ if (count > bestCount) {
8583
+ best = key;
8584
+ bestCount = count;
8585
+ }
8586
+ }
8587
+ return best;
8588
+ }
8589
+ function orderingSignature(runs) {
8590
+ const counts = /* @__PURE__ */ new Map();
8591
+ for (const run of runs) {
8592
+ const signature = run.toolOrdering.join(">");
8593
+ if (signature === "") continue;
8594
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
8595
+ }
8596
+ let best;
8597
+ let bestCount = 0;
8598
+ for (const [key, count] of counts) {
8599
+ if (count > bestCount) {
8600
+ best = key;
8601
+ bestCount = count;
8602
+ }
8603
+ }
8604
+ return best;
8605
+ }
8606
+ function aggregateCohortMetrics(runs, groupKey, cohortLabel) {
8607
+ const durations = runs.map((run) => run.durationMs).filter((value) => typeof value === "number");
8608
+ const tokenValues = runs.map((run) => run.tokenUsageTotal).filter((value) => typeof value === "number");
8609
+ const errors = runs.filter((run) => run.error).length;
8610
+ const observationFailures = runs.reduce((sum, run) => sum + run.observationFailures, 0);
8611
+ return {
8612
+ groupKey,
8613
+ ...cohortLabel !== void 0 ? { cohortLabel } : {},
8614
+ runCount: runs.length,
8615
+ errorRate: runs.length > 0 ? errors / runs.length : 0,
8616
+ avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : void 0,
8617
+ p95DurationMs: percentile2(durations, 95),
8618
+ avgLlmCallCount: runs.length > 0 ? runs.reduce((sum, run) => sum + run.llmCallCount, 0) / runs.length : 0,
8619
+ avgTokenUsage: tokenValues.length > 0 ? tokenValues.reduce((sum, value) => sum + value, 0) / tokenValues.length : void 0,
8620
+ avgRetryCount: runs.length > 0 ? runs.reduce((sum, run) => sum + run.retryCount, 0) / runs.length : 0,
8621
+ observationFailureRate: runs.length > 0 ? observationFailures / runs.length : 0,
8622
+ avgGuardrailFailures: runs.length > 0 ? runs.reduce((sum, run) => sum + run.guardrailFailures, 0) / runs.length : 0,
8623
+ avgCircuitViolations: runs.length > 0 ? runs.reduce((sum, run) => sum + run.circuitViolations, 0) / runs.length : 0,
8624
+ avgRedactionWarnings: runs.length > 0 ? runs.reduce((sum, run) => sum + run.redactionWarnings, 0) / runs.length : 0,
8625
+ dominantToolChoice: dominantToolChoice(runs),
8626
+ toolOrderingSignature: orderingSignature(runs)
8627
+ };
8628
+ }
8629
+ var init_metrics = __esm({
8630
+ "packages/core/src/cohort/metrics.ts"() {
8631
+ init_extract();
8632
+ init_trace_metadata();
8633
+ init_storage();
8634
+ }
8635
+ });
8636
+
8637
+ // packages/core/src/cohort/analyze.ts
8638
+ function normalizeMetrics(metrics) {
8639
+ if (metrics === void 0 || metrics.length === 0) return [...DEFAULT_METRICS];
8640
+ const allowed = new Set(COHORT_METRIC_IDS);
8641
+ return metrics.filter((metric) => allowed.has(metric));
8642
+ }
8643
+ async function analyzeCohort(runsInput, options) {
8644
+ const cohortKey = options.cohortKey ?? "cohort";
8645
+ const groupBySpec = parseGroupBySpec(options.groupBy);
8646
+ const metrics = normalizeMetrics(options.metrics);
8647
+ const { runs: filteredRuns, warnings } = filterRunsForCohort(runsInput, {
8648
+ cohortKey,
8649
+ baseline: options.baseline,
8650
+ candidate: options.candidate
8651
+ });
8652
+ const runMetrics = [];
8653
+ for (const run of filteredRuns) {
8654
+ if (run.filePath === void 0) continue;
8655
+ const cohortLabel = resolveCohortLabel(
8656
+ run,
8657
+ cohortKey,
8658
+ options.baseline,
8659
+ options.candidate
8660
+ );
8661
+ runMetrics.push(
8662
+ await computeCohortRunMetrics({
8663
+ runId: run.runId,
8664
+ filePath: run.filePath,
8665
+ metadata: run.metadata,
8666
+ status: run.status,
8667
+ durationMs: run.durationMs,
8668
+ groupKey: resolveRunGroupKey(run, groupBySpec),
8669
+ cohortLabel
8670
+ })
8671
+ );
8672
+ }
8673
+ const groupMap = /* @__PURE__ */ new Map();
8674
+ for (const run of runMetrics) {
8675
+ const key = `${run.cohortLabel ?? "*"}::${run.groupKey}`;
8676
+ const bucket = groupMap.get(key) ?? [];
8677
+ bucket.push(run);
8678
+ groupMap.set(key, bucket);
8679
+ }
8680
+ const groups = [...groupMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
8681
+ ([, bucket]) => aggregateCohortMetrics(
8682
+ bucket,
8683
+ bucket[0].groupKey,
8684
+ bucket[0]?.cohortLabel
8685
+ )
8686
+ );
8687
+ const comparisons = options.baseline !== void 0 && options.candidate !== void 0 ? (() => {
8688
+ const groupKeys = [
8689
+ ...new Set(groups.map((group) => group.groupKey))
8690
+ ].sort((a, b) => a.localeCompare(b));
8691
+ const items = [];
8692
+ for (const groupKey of groupKeys) {
8693
+ items.push(
8694
+ ...compareCohortAggregates(groups, {
8695
+ baseline: options.baseline,
8696
+ candidate: options.candidate,
8697
+ metrics,
8698
+ groupKey
8699
+ })
8700
+ );
8701
+ }
8702
+ return items;
8703
+ })() : [];
8704
+ const regression = comparisons.some((item) => item.regression);
8705
+ return {
8706
+ ok: !regression,
8707
+ traceDir: options.traceDir,
8708
+ ...options.baseline !== void 0 ? { baseline: options.baseline } : {},
8709
+ ...options.candidate !== void 0 ? { candidate: options.candidate } : {},
8710
+ cohortKey,
8711
+ groupBy: options.groupBy ?? "model",
8712
+ metrics,
8713
+ groups,
8714
+ comparisons,
8715
+ runs: runMetrics,
8716
+ warnings
8717
+ };
8718
+ }
8719
+ var DEFAULT_METRICS;
8720
+ var init_analyze = __esm({
8721
+ "packages/core/src/cohort/analyze.ts"() {
8722
+ init_compare();
8723
+ init_grouping();
8724
+ init_metrics();
8725
+ init_types5();
8726
+ DEFAULT_METRICS = [
8727
+ "errorRate",
8728
+ "duration",
8729
+ "toolChoice",
8730
+ "observationFailure"
8731
+ ];
8732
+ }
8733
+ });
8734
+
8735
+ // packages/core/src/exporters/helpers.ts
8736
+ function shouldRedactKey(key) {
8737
+ const k = key.toLowerCase();
8738
+ for (const s of REDACT_SUBSTRINGS) {
8739
+ if (k.includes(s)) return true;
8740
+ }
8741
+ return false;
8742
+ }
8743
+ function safeString(value, maxLength) {
8744
+ if (value === null || value === void 0) return "";
8745
+ let s;
8746
+ if (typeof value === "string") s = value;
8747
+ else if (typeof value === "number" || typeof value === "boolean") s = String(value);
8748
+ else s = stableJson(value, false);
8749
+ if (maxLength !== void 0 && maxLength >= 0 && s.length > maxLength) {
8750
+ return `${s.slice(0, maxLength)}\u2026`;
8751
+ }
8752
+ return s;
8753
+ }
8754
+ function escapeMarkdown(value) {
8755
+ return value.replace(/\|/g, "\\|").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, " ");
8756
+ }
8757
+ function escapeHtml(value) {
8758
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8759
+ }
8760
+ function sortKeysDeep(input3) {
8761
+ if (input3 === null || typeof input3 !== "object") return input3;
8762
+ if (Array.isArray(input3)) return input3.map(sortKeysDeep);
8763
+ const o = input3;
8764
+ const out = {};
8765
+ for (const k of Object.keys(o).sort()) {
8766
+ out[k] = sortKeysDeep(o[k]);
8767
+ }
8768
+ return out;
8769
+ }
8770
+ function stableJson(value, pretty) {
8771
+ const sorted = sortKeysDeep(value);
8772
+ return pretty === true ? JSON.stringify(sorted, null, 2) : JSON.stringify(sorted);
8773
+ }
8774
+ function compactAttributes3(attrs, options) {
8775
+ if (attrs === void 0) return {};
8776
+ const maxLen = options?.maxLength ?? 500;
8777
+ const redacted = options?.redacted ?? true;
8778
+ const out = {};
8779
+ for (const key of Object.keys(attrs).sort()) {
8780
+ if (redacted && shouldRedactKey(key)) {
8781
+ out[key] = "[REDACTED]";
8782
+ continue;
8783
+ }
8784
+ const v = attrs[key];
8785
+ out[key] = compactValue(v, maxLen, redacted);
8786
+ }
8787
+ return out;
8788
+ }
8789
+ function compactValue(value, maxLen, redacted) {
8790
+ if (value === null || typeof value !== "object") {
8791
+ return typeof value === "string" ? safeString(value, maxLen) : value;
8792
+ }
8793
+ if (Array.isArray(value)) {
8794
+ const arr = value.slice(0, 20).map((x) => compactValue(x, maxLen, redacted));
8795
+ if (value.length > 20) arr.push(`\u2026(+${value.length - 20} more)`);
8796
+ return arr;
8797
+ }
8798
+ const o = value;
8799
+ const inner = {};
8800
+ for (const k of Object.keys(o)) {
8801
+ if (redacted && shouldRedactKey(k)) inner[k] = "[REDACTED]";
8802
+ else inner[k] = compactValue(o[k], maxLen, redacted);
8803
+ }
8804
+ return inner;
8805
+ }
8806
+ function flattenTree(tree) {
8807
+ const out = [];
8808
+ function walk(nodes) {
8809
+ for (const n of nodes) {
8810
+ out.push(n);
8811
+ if (n.children.length > 0) walk(n.children);
8812
+ }
8813
+ }
8814
+ walk(tree.children);
8815
+ return out;
8816
+ }
8817
+ function zeroKinds() {
8818
+ return {
8819
+ RUN: 0,
8820
+ AGENT: 0,
8821
+ LLM: 0,
8822
+ TOOL: 0,
8823
+ CHAIN: 0,
8824
+ RETRIEVER: 0,
8825
+ DECISION: 0,
8826
+ RESULT: 0,
8827
+ ERROR: 0,
8828
+ LOGIC: 0,
8829
+ LOG: 0,
8830
+ OUTCOME: 0
8831
+ };
8832
+ }
8833
+ var REDACT_SUBSTRINGS;
8834
+ var init_helpers = __esm({
8835
+ "packages/core/src/exporters/helpers.ts"() {
8836
+ REDACT_SUBSTRINGS = [
8837
+ "authorization",
8838
+ "cookie",
8839
+ "token",
8840
+ "apikey",
8841
+ "password",
8842
+ "secret",
8843
+ "email"
8844
+ ];
8845
+ }
8846
+ });
8847
+
8848
+ // packages/core/src/cohort/render.ts
8849
+ function formatRate(value) {
8850
+ if (value === void 0) return "n/a";
8851
+ return `${(value * 100).toFixed(1)}%`;
8852
+ }
8853
+ function renderCohortSummaryMarkdown(result) {
8854
+ const lines = [];
8855
+ lines.push("# Cohort analysis");
8856
+ lines.push("");
8857
+ lines.push(`Trace directory: \`${result.traceDir}\``);
8858
+ lines.push(`Group by: \`${result.groupBy}\``);
8859
+ if (result.baseline !== void 0 && result.candidate !== void 0) {
8860
+ lines.push(
8861
+ `Baseline/Candidate key: \`${result.cohortKey}\` (${result.baseline} vs ${result.candidate})`
8862
+ );
8863
+ }
8864
+ lines.push(`Status: **${result.ok ? "PASS" : "REGRESSION"}**`);
8865
+ lines.push("");
8866
+ if (result.warnings.length > 0) {
8867
+ lines.push("## Warnings");
8868
+ for (const warning of result.warnings) lines.push(`- ${warning}`);
8869
+ lines.push("");
8870
+ }
8871
+ lines.push("## Groups");
8872
+ for (const group of result.groups) {
8873
+ lines.push(
8874
+ `### ${group.cohortLabel ?? "all"} / ${group.groupKey} (${group.runCount} runs)`
8875
+ );
8876
+ lines.push(`- Error rate: ${formatRate(group.errorRate)}`);
8877
+ if (group.avgDurationMs !== void 0) {
8878
+ lines.push(`- Avg duration: ${Math.round(group.avgDurationMs)} ms`);
8879
+ }
8880
+ if (group.dominantToolChoice !== void 0) {
8881
+ lines.push(`- Dominant tools: ${group.dominantToolChoice}`);
8882
+ }
8883
+ lines.push(
8884
+ `- Observation failure rate: ${formatRate(group.observationFailureRate)}`
8885
+ );
8886
+ lines.push("");
8887
+ }
8888
+ if (result.comparisons.length > 0) {
8889
+ lines.push("## Comparisons");
8890
+ for (const comparison of result.comparisons) {
8891
+ const flag = comparison.regression ? " **REGRESSION**" : "";
8892
+ lines.push(`- ${comparison.message}${flag}`);
8893
+ }
8894
+ lines.push("");
8895
+ }
8896
+ return lines.join("\n").trimEnd();
8897
+ }
8898
+ function renderCohortReportHtml(result) {
8899
+ const rows = result.groups.map(
8900
+ (group) => `<tr><td>${escapeHtml(group.cohortLabel ?? "all")}</td><td>${escapeHtml(group.groupKey)}</td><td>${group.runCount}</td><td>${escapeHtml(formatRate(group.errorRate))}</td><td>${group.avgDurationMs !== void 0 ? Math.round(group.avgDurationMs) : "n/a"}</td></tr>`
8901
+ ).join("");
8902
+ const comparisons = result.comparisons.map(
8903
+ (item) => `<li>${escapeHtml(item.message)}${item.regression ? " <strong>REGRESSION</strong>" : ""}</li>`
8904
+ ).join("");
8905
+ return `<!DOCTYPE html>
8906
+ <html lang="en">
8907
+ <head>
8908
+ <meta charset="utf-8" />
8909
+ <title>Cohort report</title>
8910
+ <style>
8911
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
8912
+ table { border-collapse: collapse; width: 100%; }
8913
+ th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
8914
+ th { background: #f6f6f6; }
8915
+ </style>
8916
+ </head>
8917
+ <body>
8918
+ <h1>Cohort analysis</h1>
8919
+ <p>Status: <strong>${result.ok ? "PASS" : "REGRESSION"}</strong></p>
8920
+ <p>Trace directory: <code>${escapeHtml(result.traceDir)}</code></p>
8921
+ <h2>Groups</h2>
8922
+ <table>
8923
+ <thead><tr><th>Cohort</th><th>Group</th><th>Runs</th><th>Error rate</th><th>Avg duration (ms)</th></tr></thead>
8924
+ <tbody>${rows}</tbody>
8925
+ </table>
8926
+ ${result.comparisons.length > 0 ? `<h2>Comparisons</h2><ul>${comparisons}</ul>` : ""}
8927
+ </body>
8928
+ </html>`;
8929
+ }
8930
+ function renderCohortReport(result, options = {}) {
8931
+ const format = options.format ?? "markdown";
8932
+ if (format === "json") return JSON.stringify(result, null, 2);
8933
+ if (format === "html") return renderCohortReportHtml(result);
8934
+ return renderCohortSummaryMarkdown(result);
8935
+ }
8936
+ var init_render2 = __esm({
8937
+ "packages/core/src/cohort/render.ts"() {
8938
+ init_helpers();
8939
+ }
8940
+ });
8941
+
8942
+ // packages/core/src/cohort/index.ts
8943
+ var init_cohort2 = __esm({
8944
+ "packages/core/src/cohort/index.ts"() {
8945
+ init_types5();
8946
+ init_analyze();
8947
+ init_compare();
8948
+ init_grouping();
8949
+ init_metrics();
8950
+ init_render2();
8951
+ }
8952
+ });
8953
+
8278
8954
  // packages/core/src/inspect-run.ts
8279
8955
  var init_inspect_run = __esm({
8280
8956
  "packages/core/src/inspect-run.ts"() {
@@ -8320,13 +8996,14 @@ var init_advanced = __esm({
8320
8996
  init_duration();
8321
8997
  init_bundle();
8322
8998
  init_suite();
8999
+ init_cohort2();
8323
9000
  init_maybe_inspect_run();
8324
9001
  }
8325
9002
  });
8326
9003
 
8327
9004
  // packages/index-sqlite/src/types.ts
8328
9005
  var INDEX_SCHEMA_VERSION, INDEX_DB_FILENAME;
8329
- var init_types5 = __esm({
9006
+ var init_types6 = __esm({
8330
9007
  "packages/index-sqlite/src/types.ts"() {
8331
9008
  INDEX_SCHEMA_VERSION = "1";
8332
9009
  INDEX_DB_FILENAME = "trace-index.sqlite";
@@ -8384,19 +9061,19 @@ var require_file_uri_to_path = __commonJS({
8384
9061
  var rest = decodeURI(uri.substring(7));
8385
9062
  var firstSlash = rest.indexOf("/");
8386
9063
  var host = rest.substring(0, firstSlash);
8387
- var path30 = rest.substring(firstSlash + 1);
9064
+ var path31 = rest.substring(firstSlash + 1);
8388
9065
  if ("localhost" == host) host = "";
8389
9066
  if (host) {
8390
9067
  host = sep + sep + host;
8391
9068
  }
8392
- path30 = path30.replace(/^(.+)\|/, "$1:");
9069
+ path31 = path31.replace(/^(.+)\|/, "$1:");
8393
9070
  if (sep == "\\") {
8394
- path30 = path30.replace(/\//g, "\\");
9071
+ path31 = path31.replace(/\//g, "\\");
8395
9072
  }
8396
- if (/^.+\:/.test(path30)) ; else {
8397
- path30 = sep + path30;
9073
+ if (/^.+\:/.test(path31)) ; else {
9074
+ path31 = sep + path31;
8398
9075
  }
8399
- return host + path30;
9076
+ return host + path31;
8400
9077
  }
8401
9078
  }
8402
9079
  });
@@ -8405,18 +9082,18 @@ var require_file_uri_to_path = __commonJS({
8405
9082
  var require_bindings = __commonJS({
8406
9083
  "node_modules/.pnpm/bindings@1.5.0/node_modules/bindings/bindings.js"(exports$1, module) {
8407
9084
  var fs = __require("fs");
8408
- var path30 = __require("path");
9085
+ var path31 = __require("path");
8409
9086
  var fileURLToPath2 = require_file_uri_to_path();
8410
- var join = path30.join;
8411
- var dirname = path30.dirname;
8412
- var exists2 = fs.accessSync && function(path31) {
9087
+ var join = path31.join;
9088
+ var dirname = path31.dirname;
9089
+ var exists2 = fs.accessSync && function(path32) {
8413
9090
  try {
8414
- fs.accessSync(path31);
9091
+ fs.accessSync(path32);
8415
9092
  } catch (e) {
8416
9093
  return false;
8417
9094
  }
8418
9095
  return true;
8419
- } || fs.existsSync || path30.existsSync;
9096
+ } || fs.existsSync || path31.existsSync;
8420
9097
  var defaults = {
8421
9098
  arrow: process.env.NODE_BINDINGS_ARROW || " \u2192 ",
8422
9099
  compiled: process.env.NODE_BINDINGS_COMPILED_DIR || "compiled",
@@ -8461,7 +9138,7 @@ var require_bindings = __commonJS({
8461
9138
  if (!opts.module_root) {
8462
9139
  opts.module_root = exports$1.getRoot(exports$1.getFileName());
8463
9140
  }
8464
- if (path30.extname(opts.bindings) != ".node") {
9141
+ if (path31.extname(opts.bindings) != ".node") {
8465
9142
  opts.bindings += ".node";
8466
9143
  }
8467
9144
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
@@ -8696,7 +9373,7 @@ var require_pragma = __commonJS({
8696
9373
  var require_backup = __commonJS({
8697
9374
  "node_modules/.pnpm/better-sqlite3@11.10.0/node_modules/better-sqlite3/lib/methods/backup.js"(exports$1, module) {
8698
9375
  var fs = __require("fs");
8699
- var path30 = __require("path");
9376
+ var path31 = __require("path");
8700
9377
  var { promisify } = __require("util");
8701
9378
  var { cppdb } = require_util();
8702
9379
  var fsAccess = promisify(fs.access);
@@ -8712,7 +9389,7 @@ var require_backup = __commonJS({
8712
9389
  if (typeof attachedName !== "string") throw new TypeError('Expected the "attached" option to be a string');
8713
9390
  if (!attachedName) throw new TypeError('The "attached" option cannot be an empty string');
8714
9391
  if (handler != null && typeof handler !== "function") throw new TypeError('Expected the "progress" option to be a function');
8715
- await fsAccess(path30.dirname(filename)).catch(() => {
9392
+ await fsAccess(path31.dirname(filename)).catch(() => {
8716
9393
  throw new TypeError("Cannot save backup because the directory does not exist");
8717
9394
  });
8718
9395
  const isNewFile = await fsAccess(filename).then(() => false, () => true);
@@ -9012,7 +9689,7 @@ var require_inspect = __commonJS({
9012
9689
  var require_database = __commonJS({
9013
9690
  "node_modules/.pnpm/better-sqlite3@11.10.0/node_modules/better-sqlite3/lib/database.js"(exports$1, module) {
9014
9691
  var fs = __require("fs");
9015
- var path30 = __require("path");
9692
+ var path31 = __require("path");
9016
9693
  var util = require_util();
9017
9694
  var SqliteError = require_sqlite_error();
9018
9695
  var DEFAULT_ADDON;
@@ -9048,7 +9725,7 @@ var require_database = __commonJS({
9048
9725
  addon = DEFAULT_ADDON || (DEFAULT_ADDON = require_bindings()("better_sqlite3.node"));
9049
9726
  } else if (typeof nativeBinding === "string") {
9050
9727
  const requireFunc = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : __require;
9051
- addon = requireFunc(path30.resolve(nativeBinding).replace(/(\.node)?$/, ".node"));
9728
+ addon = requireFunc(path31.resolve(nativeBinding).replace(/(\.node)?$/, ".node"));
9052
9729
  } else {
9053
9730
  addon = nativeBinding;
9054
9731
  }
@@ -9056,7 +9733,7 @@ var require_database = __commonJS({
9056
9733
  addon.setErrorConstructor(SqliteError);
9057
9734
  addon.isInitialized = true;
9058
9735
  }
9059
- if (!anonymous && !fs.existsSync(path30.dirname(filename))) {
9736
+ if (!anonymous && !fs.existsSync(path31.dirname(filename))) {
9060
9737
  throw new TypeError("Cannot open database because the directory does not exist");
9061
9738
  }
9062
9739
  Object.defineProperties(this, {
@@ -9104,7 +9781,7 @@ function metaDefaults(sourceDir, fileCount) {
9104
9781
  var INDEX_SCHEMA_SQL, DERIVE_SESSIONS_SQL, META_KEYS;
9105
9782
  var init_schema = __esm({
9106
9783
  "packages/index-sqlite/src/schema.ts"() {
9107
- init_types5();
9784
+ init_types6();
9108
9785
  INDEX_SCHEMA_SQL = `
9109
9786
  DROP TABLE IF EXISTS meta;
9110
9787
  DROP TABLE IF EXISTS errors;
@@ -9357,7 +10034,7 @@ var init_builder = __esm({
9357
10034
  import_better_sqlite3 = __toESM(require_lib());
9358
10035
  init_advanced();
9359
10036
  init_schema();
9360
- init_types5();
10037
+ init_types6();
9361
10038
  DEFAULT_MAX_RUNS = 1e4;
9362
10039
  rebuildIndex = buildIndex;
9363
10040
  }
@@ -9507,14 +10184,14 @@ __export(src_exports, {
9507
10184
  });
9508
10185
  var init_src = __esm({
9509
10186
  "packages/index-sqlite/src/index.ts"() {
9510
- init_types5();
10187
+ init_types6();
9511
10188
  init_builder();
9512
10189
  init_query();
9513
10190
  }
9514
10191
  });
9515
10192
 
9516
10193
  // package.json
9517
- var version = "5.0.0";
10194
+ var version = "5.1.0";
9518
10195
 
9519
10196
  // packages/cli/src/list.ts
9520
10197
  init_advanced();
@@ -10459,7 +11136,7 @@ init_nanoid();
10459
11136
  function isFiniteNumber2(v) {
10460
11137
  return typeof v === "number" && Number.isFinite(v);
10461
11138
  }
10462
- function safeString(v) {
11139
+ function safeString2(v) {
10463
11140
  if (typeof v !== "string") return void 0;
10464
11141
  const t = v.trim();
10465
11142
  return t === "" ? void 0 : t;
@@ -10509,7 +11186,7 @@ var EventNormalizer = class {
10509
11186
  let runId;
10510
11187
  for (const k of cfg.runIdKeys) {
10511
11188
  const v = raw[k];
10512
- const s = safeString(v);
11189
+ const s = safeString2(v);
10513
11190
  if (s) {
10514
11191
  runId = s;
10515
11192
  break;
@@ -10525,7 +11202,7 @@ var EventNormalizer = class {
10525
11202
  }
10526
11203
  };
10527
11204
  }
10528
- const eventName = safeString(raw[cfg.eventKey]);
11205
+ const eventName = safeString2(raw[cfg.eventKey]);
10529
11206
  if (!eventName) {
10530
11207
  return {
10531
11208
  warning: {
@@ -10543,7 +11220,7 @@ var EventNormalizer = class {
10543
11220
  const timestamp = parsedTs ?? Date.now();
10544
11221
  const timestampMissing = parsedTs === void 0;
10545
11222
  const parentIdKey = cfg.parentIdKey;
10546
- const parentId = parentIdKey ? safeString(raw[parentIdKey]) : void 0;
11223
+ const parentId = parentIdKey ? safeString2(raw[parentIdKey]) : void 0;
10547
11224
  let durationMs2;
10548
11225
  const durationKey = cfg.durationKey;
10549
11226
  if (durationKey) {
@@ -10554,7 +11231,7 @@ var EventNormalizer = class {
10554
11231
  }
10555
11232
  let status;
10556
11233
  const statusKey = cfg.statusKey;
10557
- const statusRaw = statusKey ? safeString(raw[statusKey]) : void 0;
11234
+ const statusRaw = statusKey ? safeString2(raw[statusKey]) : void 0;
10558
11235
  if (statusRaw === "running" || statusRaw === "ok" || statusRaw === "error") {
10559
11236
  status = statusRaw;
10560
11237
  } else if (mapping?.status) {
@@ -11498,115 +12175,8 @@ function redactRunTreeForExport(tree, options) {
11498
12175
  return clone;
11499
12176
  }
11500
12177
 
11501
- // packages/core/src/exporters/helpers.ts
11502
- var REDACT_SUBSTRINGS = [
11503
- "authorization",
11504
- "cookie",
11505
- "token",
11506
- "apikey",
11507
- "password",
11508
- "secret",
11509
- "email"
11510
- ];
11511
- function shouldRedactKey(key) {
11512
- const k = key.toLowerCase();
11513
- for (const s of REDACT_SUBSTRINGS) {
11514
- if (k.includes(s)) return true;
11515
- }
11516
- return false;
11517
- }
11518
- function safeString2(value, maxLength) {
11519
- if (value === null || value === void 0) return "";
11520
- let s;
11521
- if (typeof value === "string") s = value;
11522
- else if (typeof value === "number" || typeof value === "boolean") s = String(value);
11523
- else s = stableJson(value, false);
11524
- if (maxLength !== void 0 && maxLength >= 0 && s.length > maxLength) {
11525
- return `${s.slice(0, maxLength)}\u2026`;
11526
- }
11527
- return s;
11528
- }
11529
- function escapeMarkdown(value) {
11530
- return value.replace(/\|/g, "\\|").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, " ");
11531
- }
11532
- function escapeHtml(value) {
11533
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11534
- }
11535
- function sortKeysDeep(input3) {
11536
- if (input3 === null || typeof input3 !== "object") return input3;
11537
- if (Array.isArray(input3)) return input3.map(sortKeysDeep);
11538
- const o = input3;
11539
- const out = {};
11540
- for (const k of Object.keys(o).sort()) {
11541
- out[k] = sortKeysDeep(o[k]);
11542
- }
11543
- return out;
11544
- }
11545
- function stableJson(value, pretty) {
11546
- const sorted = sortKeysDeep(value);
11547
- return pretty === true ? JSON.stringify(sorted, null, 2) : JSON.stringify(sorted);
11548
- }
11549
- function compactAttributes3(attrs, options) {
11550
- if (attrs === void 0) return {};
11551
- const maxLen = options?.maxLength ?? 500;
11552
- const redacted = options?.redacted ?? true;
11553
- const out = {};
11554
- for (const key of Object.keys(attrs).sort()) {
11555
- if (redacted && shouldRedactKey(key)) {
11556
- out[key] = "[REDACTED]";
11557
- continue;
11558
- }
11559
- const v = attrs[key];
11560
- out[key] = compactValue(v, maxLen, redacted);
11561
- }
11562
- return out;
11563
- }
11564
- function compactValue(value, maxLen, redacted) {
11565
- if (value === null || typeof value !== "object") {
11566
- return typeof value === "string" ? safeString2(value, maxLen) : value;
11567
- }
11568
- if (Array.isArray(value)) {
11569
- const arr = value.slice(0, 20).map((x) => compactValue(x, maxLen, redacted));
11570
- if (value.length > 20) arr.push(`\u2026(+${value.length - 20} more)`);
11571
- return arr;
11572
- }
11573
- const o = value;
11574
- const inner = {};
11575
- for (const k of Object.keys(o)) {
11576
- if (redacted && shouldRedactKey(k)) inner[k] = "[REDACTED]";
11577
- else inner[k] = compactValue(o[k], maxLen, redacted);
11578
- }
11579
- return inner;
11580
- }
11581
- function flattenTree(tree) {
11582
- const out = [];
11583
- function walk(nodes) {
11584
- for (const n of nodes) {
11585
- out.push(n);
11586
- if (n.children.length > 0) walk(n.children);
11587
- }
11588
- }
11589
- walk(tree.children);
11590
- return out;
11591
- }
11592
- function zeroKinds() {
11593
- return {
11594
- RUN: 0,
11595
- AGENT: 0,
11596
- LLM: 0,
11597
- TOOL: 0,
11598
- CHAIN: 0,
11599
- RETRIEVER: 0,
11600
- DECISION: 0,
11601
- RESULT: 0,
11602
- ERROR: 0,
11603
- LOGIC: 0,
11604
- LOG: 0,
11605
- OUTCOME: 0
11606
- };
11607
- }
11608
-
11609
12178
  // packages/core/src/exporters/html-exporter.ts
12179
+ init_helpers();
11610
12180
  function renderTreeHtml(nodes, ulClass = "tree") {
11611
12181
  if (nodes.length === 0) return "";
11612
12182
  const parts = [`<ul class="${ulClass}">`];
@@ -11679,7 +12249,7 @@ function exportHtml(tree, options) {
11679
12249
  if (includeErrors && errors.length > 0) {
11680
12250
  errorsHtml += "<h2>Errors</h2><ul>";
11681
12251
  for (const n of errors) {
11682
- const msg = n.event.attributes && typeof n.event.attributes.error === "object" ? safeString2(
12252
+ const msg = n.event.attributes && typeof n.event.attributes.error === "object" ? safeString(
11683
12253
  n.event.attributes.error.message,
11684
12254
  maxLen
11685
12255
  ) : "";
@@ -11744,6 +12314,7 @@ ${attrsHtml}
11744
12314
  }
11745
12315
 
11746
12316
  // packages/core/src/exporters/markdown-exporter.ts
12317
+ init_helpers();
11747
12318
  function renderTreeAscii(nodes, indent = "") {
11748
12319
  const lines = [];
11749
12320
  for (let i = 0; i < nodes.length; i++) {
@@ -11828,7 +12399,7 @@ function exportMarkdown(tree, options) {
11828
12399
  lines.push("## Errors");
11829
12400
  lines.push("");
11830
12401
  for (const n of errors) {
11831
- const msg = n.event.attributes && typeof n.event.attributes.error === "object" ? safeString2(
12402
+ const msg = n.event.attributes && typeof n.event.attributes.error === "object" ? safeString(
11832
12403
  n.event.attributes.error.message,
11833
12404
  maxLen
11834
12405
  ) : "";
@@ -11866,6 +12437,7 @@ function exportMarkdown(tree, options) {
11866
12437
  warnings
11867
12438
  };
11868
12439
  }
12440
+ init_helpers();
11869
12441
  function hexFrom(seed, byteLen) {
11870
12442
  return crypto__default.default.createHash("sha256").update(seed, "utf8").digest("hex").slice(0, byteLen * 2);
11871
12443
  }
@@ -11992,6 +12564,9 @@ function exportOpenInference(tree, options) {
11992
12564
  warnings
11993
12565
  };
11994
12566
  }
12567
+
12568
+ // packages/core/src/exporters/otlp-json-exporter.ts
12569
+ init_helpers();
11995
12570
  function hexFrom2(seed, byteLen) {
11996
12571
  return crypto__default.default.createHash("sha256").update(seed, "utf8").digest("hex").slice(0, byteLen * 2);
11997
12572
  }
@@ -12195,7 +12770,11 @@ function validateExportContent(format, content) {
12195
12770
  return { ok: false, format, errors, warnings };
12196
12771
  }
12197
12772
 
12773
+ // packages/core/src/exporters/index.ts
12774
+ init_helpers();
12775
+
12198
12776
  // packages/core/src/exporters/manual-trace-adapter.ts
12777
+ init_helpers();
12199
12778
  function stepTypeToInspectKind(t) {
12200
12779
  switch (t) {
12201
12780
  case "llm":
@@ -12375,6 +12954,7 @@ function validateExport(result) {
12375
12954
  }
12376
12955
 
12377
12956
  // packages/core/src/report.ts
12957
+ init_helpers();
12378
12958
  init_timeline();
12379
12959
  init_what();
12380
12960
  init_outcomes();
@@ -12531,6 +13111,9 @@ ${attrsSection}
12531
13111
  };
12532
13112
  }
12533
13113
 
13114
+ // packages/core/src/entries/exporters.ts
13115
+ init_helpers();
13116
+
12534
13117
  // packages/cli/src/export.ts
12535
13118
  function parseRedactionProfile(s) {
12536
13119
  const v = (s ?? "local").trim().toLowerCase();
@@ -12752,6 +13335,7 @@ function manualTraceEventsToComparableRun(events) {
12752
13335
  }
12753
13336
 
12754
13337
  // packages/core/src/diff/engine.ts
13338
+ init_helpers();
12755
13339
  var DEFAULT_THRESHOLD_MS = 0;
12756
13340
  function pathSeg(step, index) {
12757
13341
  return { index, name: step.name, stepId: step.id };
@@ -12791,13 +13375,13 @@ function pairSteps(left, right) {
12791
13375
  return pairs;
12792
13376
  }
12793
13377
  function compareLeafSteps(L, R, segments, opts, out) {
12794
- const path30 = buildPath(segments);
13378
+ const path31 = buildPath(segments);
12795
13379
  if (L.name !== R.name) {
12796
13380
  out.push({
12797
13381
  kind: "structure",
12798
13382
  severity: "warning",
12799
13383
  message: "Step name differs",
12800
- path: path30,
13384
+ path: path31,
12801
13385
  left: L.name,
12802
13386
  right: R.name
12803
13387
  });
@@ -12807,7 +13391,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12807
13391
  kind: "step-type",
12808
13392
  severity: "warning",
12809
13393
  message: "Step type differs",
12810
- path: path30,
13394
+ path: path31,
12811
13395
  left: L.type,
12812
13396
  right: R.type
12813
13397
  });
@@ -12817,7 +13401,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12817
13401
  kind: "step-status",
12818
13402
  severity: "warning",
12819
13403
  message: "Step status differs",
12820
- path: path30,
13404
+ path: path31,
12821
13405
  left: L.status,
12822
13406
  right: R.status
12823
13407
  });
@@ -12829,7 +13413,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12829
13413
  kind: "error",
12830
13414
  severity: "error",
12831
13415
  message: "Step error message differs",
12832
- path: path30,
13416
+ path: path31,
12833
13417
  left: le || void 0,
12834
13418
  right: re || void 0
12835
13419
  });
@@ -12847,7 +13431,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12847
13431
  kind: "duration",
12848
13432
  severity: "info",
12849
13433
  message: "Step duration differs",
12850
- path: path30,
13434
+ path: path31,
12851
13435
  left: ld,
12852
13436
  right: rd
12853
13437
  });
@@ -12860,7 +13444,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12860
13444
  kind: "metadata",
12861
13445
  severity: "info",
12862
13446
  message: "Step metadata differs",
12863
- path: path30,
13447
+ path: path31,
12864
13448
  left: L.metadata,
12865
13449
  right: R.metadata
12866
13450
  });
@@ -12872,7 +13456,7 @@ function compareLeafSteps(L, R, segments, opts, out) {
12872
13456
  kind: "output",
12873
13457
  severity: "info",
12874
13458
  message: "Output preview differs",
12875
- path: path30,
13459
+ path: path31,
12876
13460
  left: L.outputPreview,
12877
13461
  right: R.outputPreview
12878
13462
  });
@@ -13033,11 +13617,11 @@ function diffRuns(left, right, options) {
13033
13617
 
13034
13618
  // packages/core/src/diff/renderer.ts
13035
13619
  init_source();
13036
- function formatPath(path30) {
13037
- if (path30 === void 0 || path30.path.length === 0) {
13620
+ function formatPath(path31) {
13621
+ if (path31 === void 0 || path31.path.length === 0) {
13038
13622
  return "(run)";
13039
13623
  }
13040
- return path30.path.map((s) => s.name).join(" > ");
13624
+ return path31.path.map((s) => s.name).join(" > ");
13041
13625
  }
13042
13626
  function formatValue(v, verbose) {
13043
13627
  if (v === void 0) return "(undefined)";
@@ -14158,17 +14742,17 @@ function applyRule(rule, value, replacement) {
14158
14742
  }
14159
14743
  return value;
14160
14744
  }
14161
- function childPath(path30, key) {
14745
+ function childPath(path31, key) {
14162
14746
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
14163
- return path30 ? `${path30}.${key}` : key;
14747
+ return path31 ? `${path31}.${key}` : key;
14164
14748
  }
14165
- return `${path30 || "$"}[${JSON.stringify(key)}]`;
14749
+ return `${path31 || "$"}[${JSON.stringify(key)}]`;
14166
14750
  }
14167
- function indexPath(path30, index) {
14168
- return `${path30 || "$"}[${index}]`;
14751
+ function indexPath(path31, index) {
14752
+ return `${path31 || "$"}[${index}]`;
14169
14753
  }
14170
- function makeFinding(path30, detector, action, matchKind, severity = "warning", preview) {
14171
- return preview === void 0 ? { path: path30, detector, action, severity, matchKind } : { path: path30, detector, action, severity, matchKind, preview };
14754
+ function makeFinding(path31, detector, action, matchKind, severity = "warning", preview) {
14755
+ return preview === void 0 ? { path: path31, detector, action, severity, matchKind } : { path: path31, detector, action, severity, matchKind, preview };
14172
14756
  }
14173
14757
  function createRedactionProfile(profile = "local") {
14174
14758
  switch (profile) {
@@ -14237,11 +14821,11 @@ var Redactor2 = class {
14237
14821
  #recordFinding(state, finding) {
14238
14822
  if (this.#collectFindings) state.findings.push(finding);
14239
14823
  }
14240
- #redactValue(value, key, path30, depth, state) {
14824
+ #redactValue(value, key, path31, depth, state) {
14241
14825
  if (depth > this.#maxDepth) {
14242
14826
  this.#recordFinding(
14243
14827
  state,
14244
- makeFinding(path30, "structure.maxDepth", "truncate", "value", "warning")
14828
+ makeFinding(path31, "structure.maxDepth", "truncate", "value", "warning")
14245
14829
  );
14246
14830
  return "[Truncated]";
14247
14831
  }
@@ -14250,19 +14834,19 @@ var Redactor2 = class {
14250
14834
  if (rule) {
14251
14835
  this.#recordFinding(
14252
14836
  state,
14253
- makeFinding(path30, `key.${rule.key}`, actionForRule(rule), "key", "warning")
14837
+ makeFinding(path31, `key.${rule.key}`, actionForRule(rule), "key", "warning")
14254
14838
  );
14255
14839
  return applyRule(rule, value, this.#replacement);
14256
14840
  }
14257
14841
  }
14258
14842
  for (const detector of this.#detectors) {
14259
- const detections = detector.detect({ path: path30, key, value });
14843
+ const detections = detector.detect({ path: path31, key, value });
14260
14844
  for (const detection of detections) {
14261
14845
  const action = detection.action ?? "replace";
14262
14846
  this.#recordFinding(
14263
14847
  state,
14264
14848
  makeFinding(
14265
- path30,
14849
+ path31,
14266
14850
  detector.id,
14267
14851
  action,
14268
14852
  detection.matchKind ?? detector.matchKind ?? "custom",
@@ -14280,7 +14864,7 @@ var Redactor2 = class {
14280
14864
  const out = [];
14281
14865
  state.seen.set(value, out);
14282
14866
  value.forEach((item, index) => {
14283
- out[index] = this.#redactValue(item, void 0, indexPath(path30, index), depth + 1, state);
14867
+ out[index] = this.#redactValue(item, void 0, indexPath(path31, index), depth + 1, state);
14284
14868
  });
14285
14869
  return out;
14286
14870
  }
@@ -14292,7 +14876,7 @@ var Redactor2 = class {
14292
14876
  out[entryKey] = this.#redactValue(
14293
14877
  entryValue,
14294
14878
  entryKey,
14295
- childPath(path30 === "$" ? "" : path30, entryKey),
14879
+ childPath(path31 === "$" ? "" : path31, entryKey),
14296
14880
  depth + 1,
14297
14881
  state
14298
14882
  );
@@ -15233,23 +15817,23 @@ function evaluatePromptInjection(text, options = {}) {
15233
15817
  }
15234
15818
  return fail(ruleId, `Matched ${evidence.length} injection pattern(s).`, evidence, "warning");
15235
15819
  }
15236
- function validateSchemaField(value, field, path30, evidence) {
15820
+ function validateSchemaField(value, field, path31, evidence) {
15237
15821
  const ruleId = "guardrail.structured-output";
15238
15822
  if (field.type) {
15239
15823
  const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
15240
15824
  if (actual !== field.type) {
15241
- evidence.push({ ruleId, path: path30, preview: `expected ${field.type}, got ${actual}` });
15825
+ evidence.push({ ruleId, path: path31, preview: `expected ${field.type}, got ${actual}` });
15242
15826
  return;
15243
15827
  }
15244
15828
  }
15245
15829
  if (field.enum && !field.enum.some((item) => Object.is(item, value))) {
15246
- evidence.push({ ruleId, path: path30, preview: "value not in enum" });
15830
+ evidence.push({ ruleId, path: path31, preview: "value not in enum" });
15247
15831
  }
15248
15832
  if (field.type === "object" && field.required && value && typeof value === "object" && !Array.isArray(value)) {
15249
15833
  const record = value;
15250
15834
  for (const key of field.required) {
15251
15835
  if (!(key in record)) {
15252
- evidence.push({ ruleId, path: `${path30}.${key}`, preview: "missing required key" });
15836
+ evidence.push({ ruleId, path: `${path31}.${key}`, preview: "missing required key" });
15253
15837
  }
15254
15838
  }
15255
15839
  }
@@ -15750,10 +16334,10 @@ function printHuman(result) {
15750
16334
  console.log(`- ${diagnostic7.code}: ${diagnostic7.message}`);
15751
16335
  }
15752
16336
  for (const finding of result.findings) {
15753
- const path30 = finding.evidence[0]?.path;
16337
+ const path31 = finding.evidence[0]?.path;
15754
16338
  const run = finding.evidence[0]?.runId;
15755
16339
  const runPrefix = run ? `[${run}] ` : "";
15756
- console.log(`- ${runPrefix}${finding.ruleId}: ${finding.message}${path30 ? ` (${path30})` : ""}`);
16340
+ console.log(`- ${runPrefix}${finding.ruleId}: ${finding.message}${path31 ? ` (${path31})` : ""}`);
15757
16341
  }
15758
16342
  }
15759
16343
  function readErrorResult(error) {
@@ -16294,10 +16878,10 @@ async function evalRun(input3, options = {}) {
16294
16878
  diagnostics: []
16295
16879
  };
16296
16880
  }
16297
- function evidenceForRun(run, path30) {
16298
- return [{ runId: run.runId, ...path30 !== void 0 ? { path: path30 } : {} }];
16881
+ function evidenceForRun(run, path31) {
16882
+ return [{ runId: run.runId, ...path31 !== void 0 ? { path: path31 } : {} }];
16299
16883
  }
16300
- function evidenceForEvent(event, path30) {
16884
+ function evidenceForEvent(event, path31) {
16301
16885
  return [
16302
16886
  {
16303
16887
  runId: event.runId,
@@ -16305,7 +16889,7 @@ function evidenceForEvent(event, path30) {
16305
16889
  ...event.parentId !== void 0 ? { parentId: event.parentId } : {},
16306
16890
  kind: event.kind,
16307
16891
  name: event.name,
16308
- ...path30 !== void 0 ? { path: path30 } : {}
16892
+ ...path31 !== void 0 ? { path: path31 } : {}
16309
16893
  }
16310
16894
  ];
16311
16895
  }
@@ -16463,9 +17047,9 @@ function collectTextFields(nodes, keys, preferredKinds = []) {
16463
17047
  function tokenize(text) {
16464
17048
  return [...text.toLowerCase().matchAll(/[a-z0-9][a-z0-9'-]{2,}/g)].map((match) => match[0].replace(/^['-]+|['-]+$/g, "")).filter((token) => token.length > 2 && !STOP_WORDS.has(token));
16465
17049
  }
16466
- function firstEvidence(fields, run, path30) {
17050
+ function firstEvidence(fields, run, path31) {
16467
17051
  const first = fields[0];
16468
- return first === void 0 ? evidenceForRun(run, path30) : evidenceForEvent(first.node.event, first.path);
17052
+ return first === void 0 ? evidenceForRun(run, path31) : evidenceForEvent(first.node.event, first.path);
16469
17053
  }
16470
17054
  function collectSourceIds(nodes, keys) {
16471
17055
  const wanted = keySet(keys);
@@ -16842,8 +17426,8 @@ function renderEvalMarkdown(result) {
16842
17426
  if (result.findings.length > 0) {
16843
17427
  lines.push("", "## Findings");
16844
17428
  for (const finding of result.findings) {
16845
- const path30 = finding.evidence[0]?.path;
16846
- lines.push(`- ${finding.ruleId}: ${finding.message}${path30 ? ` (${path30})` : ""}`);
17429
+ const path31 = finding.evidence[0]?.path;
17430
+ lines.push(`- ${finding.ruleId}: ${finding.message}${path31 ? ` (${path31})` : ""}`);
16847
17431
  }
16848
17432
  }
16849
17433
  return `${lines.join("\n")}
@@ -17036,8 +17620,8 @@ function printHuman2(result) {
17036
17620
  console.log(`- ${diagnostic7.code}: ${diagnostic7.message}`);
17037
17621
  }
17038
17622
  for (const finding of result.findings) {
17039
- const path30 = finding.evidence[0]?.path;
17040
- console.log(`- ${finding.ruleId}: ${finding.message}${path30 ? ` (${path30})` : ""}`);
17623
+ const path31 = finding.evidence[0]?.path;
17624
+ console.log(`- ${finding.ruleId}: ${finding.message}${path31 ? ` (${path31})` : ""}`);
17041
17625
  }
17042
17626
  }
17043
17627
  function readErrorResult2(error) {
@@ -17275,8 +17859,8 @@ function printHuman3(result) {
17275
17859
  console.log(`- ${diagnostic7.code}: ${diagnostic7.message}`);
17276
17860
  }
17277
17861
  for (const finding of result.findings) {
17278
- const path30 = finding.evidence[0]?.path;
17279
- console.log(`- ${finding.ruleId}: ${finding.message}${path30 ? ` (${path30})` : ""}`);
17862
+ const path31 = finding.evidence[0]?.path;
17863
+ console.log(`- ${finding.ruleId}: ${finding.message}${path31 ? ` (${path31})` : ""}`);
17280
17864
  }
17281
17865
  console.log(`Note: ${result.note}`);
17282
17866
  }
@@ -17427,8 +18011,8 @@ function renderCheckSection(result) {
17427
18011
  `Diagnostics: ${result.diagnostics.length}`
17428
18012
  ];
17429
18013
  for (const finding of result.findings.slice(0, 10)) {
17430
- const path30 = finding.evidence[0]?.path ?? "(run)";
17431
- lines.push(`- ${finding.ruleId}: ${finding.message} (${path30})`);
18014
+ const path31 = finding.evidence[0]?.path ?? "(run)";
18015
+ lines.push(`- ${finding.ruleId}: ${finding.message} (${path31})`);
17432
18016
  }
17433
18017
  for (const diagnostic7 of result.diagnostics.slice(0, 10)) {
17434
18018
  lines.push(`- ${diagnostic7.code}: ${diagnostic7.message}`);
@@ -19878,6 +20462,88 @@ async function suiteReportCommand(options = {}) {
19878
20462
  }
19879
20463
  }
19880
20464
 
20465
+ // packages/cli/src/cohort.ts
20466
+ init_advanced();
20467
+ function normalizeMetrics2(raw) {
20468
+ const parsed = parseCohortMetricList(raw);
20469
+ if (parsed.length === 0) return void 0;
20470
+ const allowed = new Set(COHORT_METRIC_IDS);
20471
+ return parsed.filter(
20472
+ (item) => allowed.has(item)
20473
+ );
20474
+ }
20475
+ async function writeArtifacts(result, outputDir) {
20476
+ await promises.mkdir(outputDir, { recursive: true });
20477
+ const jsonPath = path23__default.default.join(outputDir, "cohort-results.json");
20478
+ const markdownPath = path23__default.default.join(outputDir, "cohort-summary.md");
20479
+ const htmlPath = path23__default.default.join(outputDir, "cohort-report.html");
20480
+ await promises.writeFile(jsonPath, `${renderCohortReport(result, { format: "json" })}
20481
+ `, "utf-8");
20482
+ await promises.writeFile(
20483
+ markdownPath,
20484
+ `${renderCohortReport(result, { format: "markdown" })}
20485
+ `,
20486
+ "utf-8"
20487
+ );
20488
+ await promises.writeFile(htmlPath, `${renderCohortReport(result, { format: "html" })}
20489
+ `, "utf-8");
20490
+ return { jsonPath, markdownPath, htmlPath };
20491
+ }
20492
+ async function cohortCommand(options = {}) {
20493
+ try {
20494
+ const traceDir = resolveTraceDir({ dir: options.dir });
20495
+ const { runs } = await loadSessionRuns(traceDir);
20496
+ const result = await analyzeCohort(runs, {
20497
+ traceDir,
20498
+ baseline: options.baseline,
20499
+ candidate: options.candidate,
20500
+ cohortKey: options.cohortKey,
20501
+ groupBy: options.groupBy,
20502
+ metrics: normalizeMetrics2(options.metric)
20503
+ });
20504
+ const format = options.format ?? (options.json ? "json" : "markdown");
20505
+ let artifacts;
20506
+ if (options.output !== void 0 && options.output.trim() !== "") {
20507
+ artifacts = await writeArtifacts(result, path23__default.default.resolve(options.output.trim()));
20508
+ }
20509
+ if (options.json || format === "json") {
20510
+ console.log(
20511
+ JSON.stringify(
20512
+ {
20513
+ ...result,
20514
+ artifacts: artifacts ?? null
20515
+ },
20516
+ null,
20517
+ 2
20518
+ )
20519
+ );
20520
+ } else if (format === "html") {
20521
+ console.log(renderCohortReport(result, { format: "html" }));
20522
+ if (artifacts !== void 0) {
20523
+ console.error(`Artifacts written to ${options.output}`);
20524
+ }
20525
+ } else {
20526
+ console.log(renderCohortReport(result, { format: "markdown" }));
20527
+ if (artifacts !== void 0) {
20528
+ console.log("");
20529
+ console.log(`Artifacts:`);
20530
+ console.log(`- ${artifacts.jsonPath}`);
20531
+ console.log(`- ${artifacts.markdownPath}`);
20532
+ console.log(`- ${artifacts.htmlPath}`);
20533
+ }
20534
+ }
20535
+ if (!result.ok) process.exitCode = 1;
20536
+ } catch (error) {
20537
+ const message = error instanceof Error ? error.message : String(error);
20538
+ if (options.json) {
20539
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
20540
+ } else {
20541
+ console.error(`[AgentInspect] cohort failed: ${message}`);
20542
+ }
20543
+ process.exitCode = 1;
20544
+ }
20545
+ }
20546
+
19881
20547
  // packages/cli/src/index.ts
19882
20548
  function runCommand(action) {
19883
20549
  void action().catch((error) => {
@@ -20311,6 +20977,12 @@ function createCliProgram() {
20311
20977
  suiteCmd.command("report").description("Render a report from a prior suite run artifact").option("--input <path>", "suite run JSON artifact").option("--format <format>", "markdown or json", "markdown").option("--json", "print JSON wrapper").action((opts) => {
20312
20978
  runCommand(() => suiteReportCommand(opts));
20313
20979
  });
20980
+ program.command("cohort").description("Compare baseline/candidate trace cohorts for regressions (v5.1+)").option("--dir <path>", "trace directory").option("--baseline <label>", "baseline cohort label (metadata key)").option("--candidate <label>", "candidate cohort label (metadata key)").option("--cohort-key <key>", "metadata key for cohort labels", "cohort").option("--group-by <spec>", "model | session | group | metadata.<key>", "model").option(
20981
+ "--metric <list>",
20982
+ "comma-separated metrics (errorRate,duration,toolChoice,...)"
20983
+ ).option("--format <format>", "markdown, json, or html", "markdown").option("-o, --output <dir>", "write cohort-results.json, cohort-summary.md, cohort-report.html").option("--json", "print deterministic JSON result").action((opts) => {
20984
+ runCommand(() => cohortCommand(opts));
20985
+ });
20314
20986
  return program;
20315
20987
  }
20316
20988
  function isPrimaryModule() {