/** * Failure clustering — turn a list of "what failed" into "why it failed". * * Why this exists * --------------- * Senior-engineer pain point: 50 samples ran, 14 failed. Reading 14 failure * cases one by one is tedious; the user wants the tool to say "8 of these * are tool-call errors, 4 are refusals, 2 are formatting errors", with a * suggested fix per cluster. * * This module: * * 1. Selects failed samples from a report (compositeScore < threshold OR * ok=false on at least one variant). * 2. Builds a compact failure description per sample (failed assertion * types + judge reason snippet + tool failure pattern). * 3. Sends the full set to a single LLM call asking it to (a) propose * N cluster labels, (b) assign each failure to a cluster, (c) suggest * a fix per cluster. * 4. Returns the structured cluster report. Pure function — no I/O, * accepts the executor as an argument. * * Design choice: single-call LLM clustering rather than embedding-based * k-means. Reasons: * * - Failure descriptions are short (~50-200 tokens each), so 14 of them * fit comfortably in one prompt. A 50-failure batch is also fine. * - The LLM can produce HUMAN-READABLE cluster labels ("tool call error", * "premature refusal") rather than numeric centroids — exactly what * the user needs to act on. * - One call = one judge cost = predictable. Embedding + clustering * needs N+1 calls minimum. * - Acceptable inaccuracy: clusters are advisory, not load-bearing. We * document this clearly and surface raw failures alongside. * * The clustering prompt is structured to be JSON-only output for parsing * stability. We do NOT include length-debias instructions — clustering is * a categorization task, not a quality scoring task, so the length-debias * directive is inapplicable. */ import type { ExecutorFn, Report } from '../types/index.js'; export interface FailureClusterRequest { report: Report; /** Executor for the clustering LLM call. */ executor: ExecutorFn; /** Model id to use for clustering. */ judgeModel: string; /** Max number of clusters. Default 5; the LLM may produce fewer. */ maxClusters?: number; /** compositeScore < this OR ok=false counts as failure. Default 3. */ failureThreshold?: number; /** Cap on how many failures to feed the LLM (selects worst by score). Default 50. */ maxFailuresFed?: number; } export interface FailureCase { sample_id: string; variant: string; /** Composite score (0 if errored). */ score: number; /** Whether the executor itself errored (ok=false). */ errored: boolean; /** Description summarizing what went wrong, fed to the LLM. */ description: string; } export interface FailureCluster { /** Human-readable cluster label, in the report's primary language. */ label: string; /** Free-text root cause analysis from the LLM. */ rootCause: string; /** Suggested fix the user can apply. */ suggestedFix: string; /** Sample IDs (with variant) belonging to this cluster. */ members: Array<{ sample_id: string; variant: string; }>; } export interface FailureClusterReport { /** All failures considered (after threshold + cap). */ failures: FailureCase[]; /** Clusters returned by the LLM, sorted by member count desc. */ clusters: FailureCluster[]; /** Failures the LLM didn't put in any cluster (label = "other"). */ unclassified: Array<{ sample_id: string; variant: string; }>; /** USD cost of the clustering call. */ clusterCostUSD: number; /** Truncation flag — set when more failures existed than maxFailuresFed. */ truncated: boolean; totalFailures: number; } export declare function clusterFailures(req: FailureClusterRequest): Promise; export declare function formatFailureClusterReport(r: FailureClusterReport): string;