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.
@@ -3279,7 +3279,7 @@ function buildRunSummary(events) {
3279
3279
  }
3280
3280
  let totalSteps = 0;
3281
3281
  let llmSteps = 0;
3282
- let toolSteps = 0;
3282
+ let toolSteps2 = 0;
3283
3283
  let logicSteps = 0;
3284
3284
  let errorSteps = 0;
3285
3285
  let maxDepth = 0;
@@ -3309,7 +3309,7 @@ function buildRunSummary(events) {
3309
3309
  for (const [id, s] of steps.entries()) {
3310
3310
  totalSteps += 1;
3311
3311
  if (s.type === "llm") llmSteps += 1;
3312
- else if (s.type === "tool") toolSteps += 1;
3312
+ else if (s.type === "tool") toolSteps2 += 1;
3313
3313
  else logicSteps += 1;
3314
3314
  if (s.status === "error") errorSteps += 1;
3315
3315
  const depth = computeDepth(id);
@@ -3343,7 +3343,7 @@ function buildRunSummary(events) {
3343
3343
  durationMs: durationMs2,
3344
3344
  totalSteps,
3345
3345
  llmSteps,
3346
- toolSteps,
3346
+ toolSteps: toolSteps2,
3347
3347
  logicSteps,
3348
3348
  errorSteps,
3349
3349
  maxDepth,
@@ -8446,6 +8446,527 @@ function renderSuiteReport(result, options = {}) {
8446
8446
  return renderSuiteReportMarkdown(result);
8447
8447
  }
8448
8448
 
8449
+ // packages/core/src/cohort/types.ts
8450
+ var COHORT_METRIC_IDS = [
8451
+ "errorRate",
8452
+ "duration",
8453
+ "toolChoice",
8454
+ "toolOrdering",
8455
+ "llmCallCount",
8456
+ "tokenUsage",
8457
+ "retryCount",
8458
+ "observationFailure",
8459
+ "guardrailFailure",
8460
+ "circuitViolation",
8461
+ "redactionWarning"
8462
+ ];
8463
+
8464
+ // packages/core/src/cohort/compare.ts
8465
+ function compareNumber(metric, label, baseline, candidate, higherIsWorse = true) {
8466
+ if (baseline === void 0 && candidate === void 0) return void 0;
8467
+ const delta = baseline !== void 0 && candidate !== void 0 ? candidate - baseline : void 0;
8468
+ const regression = delta !== void 0 && (higherIsWorse && delta > 0 || !higherIsWorse && delta < 0);
8469
+ return {
8470
+ metric,
8471
+ baseline,
8472
+ candidate,
8473
+ delta,
8474
+ regression,
8475
+ message: `${label}: baseline=${baseline ?? "n/a"} candidate=${candidate ?? "n/a"}${delta !== void 0 ? ` (delta ${delta})` : ""}`
8476
+ };
8477
+ }
8478
+ function pickAggregate(groups, cohortLabel, groupKey) {
8479
+ return groups.find(
8480
+ (group) => group.cohortLabel === cohortLabel && (groupKey === void 0 || group.groupKey === groupKey)
8481
+ );
8482
+ }
8483
+ function compareCohortAggregates(groups, options) {
8484
+ const baselineAgg = pickAggregate(groups, options.baseline, options.groupKey);
8485
+ const candidateAgg = pickAggregate(groups, options.candidate, options.groupKey);
8486
+ const comparisons = [];
8487
+ for (const metric of options.metrics) {
8488
+ switch (metric) {
8489
+ case "errorRate": {
8490
+ const item = compareNumber(
8491
+ metric,
8492
+ "Error rate",
8493
+ baselineAgg?.errorRate,
8494
+ candidateAgg?.errorRate
8495
+ );
8496
+ if (item) comparisons.push(item);
8497
+ break;
8498
+ }
8499
+ case "duration": {
8500
+ const item = compareNumber(
8501
+ metric,
8502
+ "Average duration (ms)",
8503
+ baselineAgg?.avgDurationMs,
8504
+ candidateAgg?.avgDurationMs
8505
+ );
8506
+ if (item) comparisons.push(item);
8507
+ break;
8508
+ }
8509
+ case "llmCallCount": {
8510
+ const item = compareNumber(
8511
+ metric,
8512
+ "Average LLM calls",
8513
+ baselineAgg?.avgLlmCallCount,
8514
+ candidateAgg?.avgLlmCallCount
8515
+ );
8516
+ if (item) comparisons.push(item);
8517
+ break;
8518
+ }
8519
+ case "tokenUsage": {
8520
+ const item = compareNumber(
8521
+ metric,
8522
+ "Average token usage",
8523
+ baselineAgg?.avgTokenUsage,
8524
+ candidateAgg?.avgTokenUsage
8525
+ );
8526
+ if (item) comparisons.push(item);
8527
+ break;
8528
+ }
8529
+ case "retryCount": {
8530
+ const item = compareNumber(
8531
+ metric,
8532
+ "Average retries",
8533
+ baselineAgg?.avgRetryCount,
8534
+ candidateAgg?.avgRetryCount
8535
+ );
8536
+ if (item) comparisons.push(item);
8537
+ break;
8538
+ }
8539
+ case "observationFailure": {
8540
+ const item = compareNumber(
8541
+ metric,
8542
+ "Observation failure rate",
8543
+ baselineAgg?.observationFailureRate,
8544
+ candidateAgg?.observationFailureRate
8545
+ );
8546
+ if (item) comparisons.push(item);
8547
+ break;
8548
+ }
8549
+ case "toolChoice": {
8550
+ const baselineValue = baselineAgg?.dominantToolChoice;
8551
+ const candidateValue = candidateAgg?.dominantToolChoice;
8552
+ comparisons.push({
8553
+ metric,
8554
+ baseline: baselineValue,
8555
+ candidate: candidateValue,
8556
+ delta: baselineValue === candidateValue ? "same" : "changed",
8557
+ regression: baselineValue !== candidateValue,
8558
+ message: `Tool choice: baseline=${baselineValue ?? "n/a"} candidate=${candidateValue ?? "n/a"}`
8559
+ });
8560
+ break;
8561
+ }
8562
+ case "toolOrdering": {
8563
+ const baselineValue = baselineAgg?.toolOrderingSignature;
8564
+ const candidateValue = candidateAgg?.toolOrderingSignature;
8565
+ comparisons.push({
8566
+ metric,
8567
+ baseline: baselineValue,
8568
+ candidate: candidateValue,
8569
+ delta: baselineValue === candidateValue ? "same" : "changed",
8570
+ regression: baselineValue !== candidateValue,
8571
+ message: `Tool ordering: baseline=${baselineValue ?? "n/a"} candidate=${candidateValue ?? "n/a"}`
8572
+ });
8573
+ break;
8574
+ }
8575
+ case "guardrailFailure": {
8576
+ const item = compareNumber(
8577
+ metric,
8578
+ "Guardrail failures",
8579
+ baselineAgg?.avgGuardrailFailures,
8580
+ candidateAgg?.avgGuardrailFailures
8581
+ );
8582
+ if (item) comparisons.push(item);
8583
+ break;
8584
+ }
8585
+ case "circuitViolation": {
8586
+ const item = compareNumber(
8587
+ metric,
8588
+ "Circuit violations",
8589
+ baselineAgg?.avgCircuitViolations,
8590
+ candidateAgg?.avgCircuitViolations
8591
+ );
8592
+ if (item) comparisons.push(item);
8593
+ break;
8594
+ }
8595
+ case "redactionWarning": {
8596
+ const item = compareNumber(
8597
+ metric,
8598
+ "Redaction warnings",
8599
+ baselineAgg?.avgRedactionWarnings,
8600
+ candidateAgg?.avgRedactionWarnings
8601
+ );
8602
+ if (item) comparisons.push(item);
8603
+ break;
8604
+ }
8605
+ }
8606
+ }
8607
+ return comparisons;
8608
+ }
8609
+
8610
+ // packages/core/src/cohort/grouping.ts
8611
+ function parseCohortMetricList(value) {
8612
+ if (value === void 0 || value.trim() === "") return [];
8613
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
8614
+ }
8615
+ function parseGroupBySpec(groupBy) {
8616
+ const raw = (groupBy ?? "model").trim();
8617
+ if (raw === "model") return { kind: "model" };
8618
+ if (raw === "session") return { kind: "session" };
8619
+ if (raw === "group") return { kind: "group" };
8620
+ if (raw.startsWith("metadata.")) {
8621
+ const metadataKey = raw.slice("metadata.".length).trim();
8622
+ if (metadataKey === "") throw new Error("metadata group-by requires a key.");
8623
+ return { kind: "metadata", metadataKey };
8624
+ }
8625
+ throw new Error(`Unsupported --group-by value: ${raw}`);
8626
+ }
8627
+ function metadataString(metadata, key) {
8628
+ const value = metadata?.[key];
8629
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
8630
+ }
8631
+ function resolveRunGroupKey(run, groupBy) {
8632
+ const metadata = run.metadata ?? {};
8633
+ switch (groupBy.kind) {
8634
+ case "model":
8635
+ return metadataString(metadata, "model") ?? "unknown";
8636
+ case "session":
8637
+ return extractSessionWorkflowMetadata(metadata)?.sessionId ?? metadataString(metadata, "sessionId") ?? "__unscoped__";
8638
+ case "group":
8639
+ return extractSessionWorkflowMetadata(metadata)?.groupId ?? metadataString(metadata, "groupId") ?? "__unscoped__";
8640
+ case "metadata":
8641
+ return metadataString(metadata, groupBy.metadataKey) ?? "__missing__";
8642
+ default:
8643
+ return "unknown";
8644
+ }
8645
+ }
8646
+ function resolveCohortLabel(run, cohortKey, baseline, candidate) {
8647
+ const label = metadataString(run.metadata, cohortKey);
8648
+ if (label === void 0) return void 0;
8649
+ if (baseline !== void 0 && label === baseline) return baseline;
8650
+ if (candidate !== void 0 && label === candidate) return candidate;
8651
+ if (baseline === void 0 && candidate === void 0) return label;
8652
+ return void 0;
8653
+ }
8654
+ function filterRunsForCohort(runs, options) {
8655
+ const warnings = [];
8656
+ if (options.baseline === void 0 && options.candidate === void 0) {
8657
+ return { runs: [...runs], warnings };
8658
+ }
8659
+ const selected = [];
8660
+ for (const run of runs) {
8661
+ const label = resolveCohortLabel(
8662
+ run,
8663
+ options.cohortKey,
8664
+ options.baseline,
8665
+ options.candidate
8666
+ );
8667
+ if (label !== void 0) selected.push(run);
8668
+ }
8669
+ if (selected.length === 0) {
8670
+ warnings.push(
8671
+ `No runs matched baseline/candidate labels on metadata.${options.cohortKey}.`
8672
+ );
8673
+ }
8674
+ return { runs: selected, warnings };
8675
+ }
8676
+
8677
+ // packages/core/src/cohort/metrics.ts
8678
+ function asNumber(value) {
8679
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8680
+ }
8681
+ function toolSteps(events) {
8682
+ const ordering = [];
8683
+ const choices = /* @__PURE__ */ new Set();
8684
+ const sorted = [...events].filter((event) => event.event === "step_started").sort((a, b) => a.timestamp - b.timestamp);
8685
+ for (const event of sorted) {
8686
+ const step = event;
8687
+ if (step.type !== "tool") continue;
8688
+ const name = typeof step.metadata?.toolName === "string" ? step.metadata.toolName : step.name;
8689
+ ordering.push(name);
8690
+ choices.add(name);
8691
+ }
8692
+ return { choices: [...choices].sort(), ordering };
8693
+ }
8694
+ async function computeCohortRunMetrics(input) {
8695
+ const events = await readTraceEventsFromFile(input.filePath);
8696
+ const summary = buildRunSummary(events);
8697
+ const tools = toolSteps(events);
8698
+ const outcomes = extractOutcomesFromTraceEvents(events);
8699
+ const observationFailures = outcomes.filter((item) => item.status === "failed").length;
8700
+ const metadata = input.metadata ?? {};
8701
+ const retryCount = asNumber(metadata.attempt) !== void 0 && asNumber(metadata.attempt) > 1 ? asNumber(metadata.attempt) - 1 : typeof metadata.retryOf === "string" ? 1 : 0;
8702
+ return {
8703
+ runId: input.runId,
8704
+ ...input.cohortLabel !== void 0 ? { cohortLabel: input.cohortLabel } : {},
8705
+ groupKey: input.groupKey,
8706
+ status: summary.status,
8707
+ error: summary.status === "error",
8708
+ durationMs: summary.durationMs ?? input.durationMs,
8709
+ llmCallCount: summary.llmSteps,
8710
+ tokenUsageTotal: summary.totalTokens?.total,
8711
+ retryCount,
8712
+ observationFailures,
8713
+ guardrailFailures: asNumber(metadata.guardrailFailures) ?? 0,
8714
+ circuitViolations: asNumber(metadata.circuitViolations) ?? 0,
8715
+ redactionWarnings: asNumber(metadata.redactionWarnings) ?? 0,
8716
+ toolChoices: tools.choices,
8717
+ toolOrdering: tools.ordering
8718
+ };
8719
+ }
8720
+ function percentile2(values, p) {
8721
+ if (values.length === 0) return void 0;
8722
+ const sorted = [...values].sort((a, b) => a - b);
8723
+ const idx = Math.min(
8724
+ sorted.length - 1,
8725
+ Math.max(0, Math.ceil(p / 100 * sorted.length) - 1)
8726
+ );
8727
+ return sorted[idx];
8728
+ }
8729
+ function dominantToolChoice(runs) {
8730
+ const counts = /* @__PURE__ */ new Map();
8731
+ for (const run of runs) {
8732
+ const signature = run.toolChoices.join(",");
8733
+ if (signature === "") continue;
8734
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
8735
+ }
8736
+ let best;
8737
+ let bestCount = 0;
8738
+ for (const [key, count] of counts) {
8739
+ if (count > bestCount) {
8740
+ best = key;
8741
+ bestCount = count;
8742
+ }
8743
+ }
8744
+ return best;
8745
+ }
8746
+ function orderingSignature(runs) {
8747
+ const counts = /* @__PURE__ */ new Map();
8748
+ for (const run of runs) {
8749
+ const signature = run.toolOrdering.join(">");
8750
+ if (signature === "") continue;
8751
+ counts.set(signature, (counts.get(signature) ?? 0) + 1);
8752
+ }
8753
+ let best;
8754
+ let bestCount = 0;
8755
+ for (const [key, count] of counts) {
8756
+ if (count > bestCount) {
8757
+ best = key;
8758
+ bestCount = count;
8759
+ }
8760
+ }
8761
+ return best;
8762
+ }
8763
+ function aggregateCohortMetrics(runs, groupKey, cohortLabel) {
8764
+ const durations = runs.map((run) => run.durationMs).filter((value) => typeof value === "number");
8765
+ const tokenValues = runs.map((run) => run.tokenUsageTotal).filter((value) => typeof value === "number");
8766
+ const errors = runs.filter((run) => run.error).length;
8767
+ const observationFailures = runs.reduce((sum, run) => sum + run.observationFailures, 0);
8768
+ return {
8769
+ groupKey,
8770
+ ...cohortLabel !== void 0 ? { cohortLabel } : {},
8771
+ runCount: runs.length,
8772
+ errorRate: runs.length > 0 ? errors / runs.length : 0,
8773
+ avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : void 0,
8774
+ p95DurationMs: percentile2(durations, 95),
8775
+ avgLlmCallCount: runs.length > 0 ? runs.reduce((sum, run) => sum + run.llmCallCount, 0) / runs.length : 0,
8776
+ avgTokenUsage: tokenValues.length > 0 ? tokenValues.reduce((sum, value) => sum + value, 0) / tokenValues.length : void 0,
8777
+ avgRetryCount: runs.length > 0 ? runs.reduce((sum, run) => sum + run.retryCount, 0) / runs.length : 0,
8778
+ observationFailureRate: runs.length > 0 ? observationFailures / runs.length : 0,
8779
+ avgGuardrailFailures: runs.length > 0 ? runs.reduce((sum, run) => sum + run.guardrailFailures, 0) / runs.length : 0,
8780
+ avgCircuitViolations: runs.length > 0 ? runs.reduce((sum, run) => sum + run.circuitViolations, 0) / runs.length : 0,
8781
+ avgRedactionWarnings: runs.length > 0 ? runs.reduce((sum, run) => sum + run.redactionWarnings, 0) / runs.length : 0,
8782
+ dominantToolChoice: dominantToolChoice(runs),
8783
+ toolOrderingSignature: orderingSignature(runs)
8784
+ };
8785
+ }
8786
+
8787
+ // packages/core/src/cohort/analyze.ts
8788
+ var DEFAULT_METRICS = [
8789
+ "errorRate",
8790
+ "duration",
8791
+ "toolChoice",
8792
+ "observationFailure"
8793
+ ];
8794
+ function normalizeMetrics(metrics) {
8795
+ if (metrics === void 0 || metrics.length === 0) return [...DEFAULT_METRICS];
8796
+ const allowed = new Set(COHORT_METRIC_IDS);
8797
+ return metrics.filter((metric) => allowed.has(metric));
8798
+ }
8799
+ async function analyzeCohort(runsInput, options) {
8800
+ const cohortKey = options.cohortKey ?? "cohort";
8801
+ const groupBySpec = parseGroupBySpec(options.groupBy);
8802
+ const metrics = normalizeMetrics(options.metrics);
8803
+ const { runs: filteredRuns, warnings } = filterRunsForCohort(runsInput, {
8804
+ cohortKey,
8805
+ baseline: options.baseline,
8806
+ candidate: options.candidate
8807
+ });
8808
+ const runMetrics = [];
8809
+ for (const run of filteredRuns) {
8810
+ if (run.filePath === void 0) continue;
8811
+ const cohortLabel = resolveCohortLabel(
8812
+ run,
8813
+ cohortKey,
8814
+ options.baseline,
8815
+ options.candidate
8816
+ );
8817
+ runMetrics.push(
8818
+ await computeCohortRunMetrics({
8819
+ runId: run.runId,
8820
+ filePath: run.filePath,
8821
+ metadata: run.metadata,
8822
+ status: run.status,
8823
+ durationMs: run.durationMs,
8824
+ groupKey: resolveRunGroupKey(run, groupBySpec),
8825
+ cohortLabel
8826
+ })
8827
+ );
8828
+ }
8829
+ const groupMap = /* @__PURE__ */ new Map();
8830
+ for (const run of runMetrics) {
8831
+ const key = `${run.cohortLabel ?? "*"}::${run.groupKey}`;
8832
+ const bucket = groupMap.get(key) ?? [];
8833
+ bucket.push(run);
8834
+ groupMap.set(key, bucket);
8835
+ }
8836
+ const groups = [...groupMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
8837
+ ([, bucket]) => aggregateCohortMetrics(
8838
+ bucket,
8839
+ bucket[0].groupKey,
8840
+ bucket[0]?.cohortLabel
8841
+ )
8842
+ );
8843
+ const comparisons = options.baseline !== void 0 && options.candidate !== void 0 ? (() => {
8844
+ const groupKeys = [
8845
+ ...new Set(groups.map((group) => group.groupKey))
8846
+ ].sort((a, b) => a.localeCompare(b));
8847
+ const items = [];
8848
+ for (const groupKey of groupKeys) {
8849
+ items.push(
8850
+ ...compareCohortAggregates(groups, {
8851
+ baseline: options.baseline,
8852
+ candidate: options.candidate,
8853
+ metrics,
8854
+ groupKey
8855
+ })
8856
+ );
8857
+ }
8858
+ return items;
8859
+ })() : [];
8860
+ const regression = comparisons.some((item) => item.regression);
8861
+ return {
8862
+ ok: !regression,
8863
+ traceDir: options.traceDir,
8864
+ ...options.baseline !== void 0 ? { baseline: options.baseline } : {},
8865
+ ...options.candidate !== void 0 ? { candidate: options.candidate } : {},
8866
+ cohortKey,
8867
+ groupBy: options.groupBy ?? "model",
8868
+ metrics,
8869
+ groups,
8870
+ comparisons,
8871
+ runs: runMetrics,
8872
+ warnings
8873
+ };
8874
+ }
8875
+
8876
+ // packages/core/src/exporters/helpers.ts
8877
+ function escapeHtml(value) {
8878
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8879
+ }
8880
+
8881
+ // packages/core/src/cohort/render.ts
8882
+ function formatRate(value) {
8883
+ if (value === void 0) return "n/a";
8884
+ return `${(value * 100).toFixed(1)}%`;
8885
+ }
8886
+ function renderCohortSummaryMarkdown(result) {
8887
+ const lines = [];
8888
+ lines.push("# Cohort analysis");
8889
+ lines.push("");
8890
+ lines.push(`Trace directory: \`${result.traceDir}\``);
8891
+ lines.push(`Group by: \`${result.groupBy}\``);
8892
+ if (result.baseline !== void 0 && result.candidate !== void 0) {
8893
+ lines.push(
8894
+ `Baseline/Candidate key: \`${result.cohortKey}\` (${result.baseline} vs ${result.candidate})`
8895
+ );
8896
+ }
8897
+ lines.push(`Status: **${result.ok ? "PASS" : "REGRESSION"}**`);
8898
+ lines.push("");
8899
+ if (result.warnings.length > 0) {
8900
+ lines.push("## Warnings");
8901
+ for (const warning of result.warnings) lines.push(`- ${warning}`);
8902
+ lines.push("");
8903
+ }
8904
+ lines.push("## Groups");
8905
+ for (const group of result.groups) {
8906
+ lines.push(
8907
+ `### ${group.cohortLabel ?? "all"} / ${group.groupKey} (${group.runCount} runs)`
8908
+ );
8909
+ lines.push(`- Error rate: ${formatRate(group.errorRate)}`);
8910
+ if (group.avgDurationMs !== void 0) {
8911
+ lines.push(`- Avg duration: ${Math.round(group.avgDurationMs)} ms`);
8912
+ }
8913
+ if (group.dominantToolChoice !== void 0) {
8914
+ lines.push(`- Dominant tools: ${group.dominantToolChoice}`);
8915
+ }
8916
+ lines.push(
8917
+ `- Observation failure rate: ${formatRate(group.observationFailureRate)}`
8918
+ );
8919
+ lines.push("");
8920
+ }
8921
+ if (result.comparisons.length > 0) {
8922
+ lines.push("## Comparisons");
8923
+ for (const comparison of result.comparisons) {
8924
+ const flag = comparison.regression ? " **REGRESSION**" : "";
8925
+ lines.push(`- ${comparison.message}${flag}`);
8926
+ }
8927
+ lines.push("");
8928
+ }
8929
+ return lines.join("\n").trimEnd();
8930
+ }
8931
+ function renderCohortReportHtml(result) {
8932
+ const rows = result.groups.map(
8933
+ (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>`
8934
+ ).join("");
8935
+ const comparisons = result.comparisons.map(
8936
+ (item) => `<li>${escapeHtml(item.message)}${item.regression ? " <strong>REGRESSION</strong>" : ""}</li>`
8937
+ ).join("");
8938
+ return `<!DOCTYPE html>
8939
+ <html lang="en">
8940
+ <head>
8941
+ <meta charset="utf-8" />
8942
+ <title>Cohort report</title>
8943
+ <style>
8944
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
8945
+ table { border-collapse: collapse; width: 100%; }
8946
+ th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
8947
+ th { background: #f6f6f6; }
8948
+ </style>
8949
+ </head>
8950
+ <body>
8951
+ <h1>Cohort analysis</h1>
8952
+ <p>Status: <strong>${result.ok ? "PASS" : "REGRESSION"}</strong></p>
8953
+ <p>Trace directory: <code>${escapeHtml(result.traceDir)}</code></p>
8954
+ <h2>Groups</h2>
8955
+ <table>
8956
+ <thead><tr><th>Cohort</th><th>Group</th><th>Runs</th><th>Error rate</th><th>Avg duration (ms)</th></tr></thead>
8957
+ <tbody>${rows}</tbody>
8958
+ </table>
8959
+ ${result.comparisons.length > 0 ? `<h2>Comparisons</h2><ul>${comparisons}</ul>` : ""}
8960
+ </body>
8961
+ </html>`;
8962
+ }
8963
+ function renderCohortReport(result, options = {}) {
8964
+ const format = options.format ?? "markdown";
8965
+ if (format === "json") return JSON.stringify(result, null, 2);
8966
+ if (format === "html") return renderCohortReportHtml(result);
8967
+ return renderCohortSummaryMarkdown(result);
8968
+ }
8969
+
8449
8970
  // packages/core/src/inspect-run.ts
8450
8971
  function normalizeRunName(name) {
8451
8972
  if (typeof name !== "string" || name.trim() === "") {
@@ -8576,6 +9097,7 @@ async function maybeInspectRun(name, fn, options) {
8576
9097
  return inspectRun(name, fn, options);
8577
9098
  }
8578
9099
 
9100
+ exports.COHORT_METRIC_IDS = COHORT_METRIC_IDS;
8579
9101
  exports.DEFAULT_MAX_EVENT_BYTES = DEFAULT_MAX_EVENT_BYTES;
8580
9102
  exports.DEFAULT_MAX_METADATA_VALUE_LENGTH = DEFAULT_MAX_METADATA_VALUE_LENGTH;
8581
9103
  exports.DEFAULT_MAX_PREVIEW_LENGTH = DEFAULT_MAX_PREVIEW_LENGTH;
@@ -8592,6 +9114,7 @@ exports.TERMINAL_INDENT = TERMINAL_INDENT;
8592
9114
  exports.TraceDirectory = TraceDirectory;
8593
9115
  exports.aggregateBundleSafeStatus = aggregateBundleSafeStatus;
8594
9116
  exports.aggregateSessionCheckResults = aggregateSessionCheckResults;
9117
+ exports.analyzeCohort = analyzeCohort;
8595
9118
  exports.buildActivitySummary = buildActivitySummary;
8596
9119
  exports.buildBundleMetadata = buildBundleMetadata;
8597
9120
  exports.buildBundleSummaryMarkdown = buildBundleSummaryMarkdown;
@@ -8603,6 +9126,7 @@ exports.buildRunWhatSummary = buildRunWhatSummary;
8603
9126
  exports.buildSessionIndex = buildSessionIndex;
8604
9127
  exports.buildTraceStats = buildTraceStats;
8605
9128
  exports.bundleFailsOnSafety = bundleFailsOnSafety;
9129
+ exports.compareCohortAggregates = compareCohortAggregates;
8606
9130
  exports.createInspector = createInspector;
8607
9131
  exports.createInspectorRuntime = createInspectorRuntime;
8608
9132
  exports.createRunId = createRunId;
@@ -8650,8 +9174,10 @@ exports.loadTraceMetadataList = loadTraceMetadataList;
8650
9174
  exports.maybeInspectRun = maybeInspectRun;
8651
9175
  exports.normalizeBundleOutputPath = normalizeBundleOutputPath;
8652
9176
  exports.normalizeSuiteConfig = normalizeSuiteConfig;
9177
+ exports.parseCohortMetricList = parseCohortMetricList;
8653
9178
  exports.parseDuration = parseDuration;
8654
9179
  exports.parseDurationFilter = parseDurationFilter;
9180
+ exports.parseGroupBySpec = parseGroupBySpec;
8655
9181
  exports.parseTraceJsonl = parseTraceJsonl;
8656
9182
  exports.prepareMetadataForDisk = prepareMetadataForDisk;
8657
9183
  exports.prepareTraceEventForDisk = prepareTraceEventForDisk;
@@ -8664,6 +9190,8 @@ exports.printStepStart = printStepStart;
8664
9190
  exports.readTraceEvents = readTraceEvents;
8665
9191
  exports.readTraceFile = readTraceFile;
8666
9192
  exports.renderActivitySummaryHuman = renderActivitySummaryHuman;
9193
+ exports.renderCohortReport = renderCohortReport;
9194
+ exports.renderCohortSummaryMarkdown = renderCohortSummaryMarkdown;
8667
9195
  exports.renderErrorLine = renderErrorLine;
8668
9196
  exports.renderRunSummary = renderRunSummary;
8669
9197
  exports.renderRunWhat = renderRunWhat;