Documentation
¶
Overview ¶
Package eval replays recorded incident cases through the investigation loop and scores whether the agent identifies the root cause — a reproducible RCA benchmark (cf. ITBench). A case records the evidence each tool returns, so the eval measures the model+loop's reasoning over fixed evidence, independent of a live cluster.
Index ¶
- Variables
- func GateError(c Campaign, failUnder float64) error
- func WriteCase(dir string, c Case) error
- type Call
- type Campaign
- type Case
- type CaseAggregate
- type CaseRecall
- type CaseWorkload
- type CompareCaseRow
- type CompareSpec
- type ComparedCase
- type ComparedRun
- type ComparisonReport
- type ComparisonRunner
- type CountingModel
- type Coverage
- type Dimension
- type Expected
- type GroundTruth
- type Judge
- type JudgeSpec
- type LiveReport
- type LiveResult
- type LiveRunner
- type ModelComparison
- type ModelEntry
- type ModelJudge
- type Prices
- type Recorder
- type Result
- type RunOutcome
- type Runner
- type Scenario
- type StepRunner
- type Trigger
- type Verdict
Constants ¶
This section is empty.
Variables ¶
var Rubric = []Dimension{
{"root_cause", 3},
{"evidence", 3},
{"solution", 3},
{"description", 3},
{"calibration", 2},
}
Rubric is the RCA-quality grading rubric (matches the design spec §5).
Functions ¶
Types ¶
type Campaign ¶
type Campaign struct {
N int
Aggregates []CaseAggregate
}
Campaign is the aggregate of a multi-repeat replay run.
func (Campaign) FlakyNames ¶
FlakyNames lists cases whose repeats disagreed too much to trust.
func (Campaign) ReachedCases ¶
ReachedCases counts cases whose pass-rate met the k-of-n bar.
type Case ¶
type Case struct {
Name string `yaml:"name"`
Prompt string `yaml:"prompt"` // the incident description (seeds the loop)
Tools map[string]string `yaml:"tools"` // tool name -> recorded evidence the tool returns
Expected Expected `yaml:"expected"`
// GroundTruth is optional live-scenario ground truth carried into replay. When
// present it unlocks the richer scoring the model-comparison benchmark reports:
// data-source coverage (expected_sources) and blind LLM-judge rubric grading
// (root_cause / expected_action). Absent ⇒ keyword-only scoring, as before.
GroundTruth *GroundTruth `yaml:"ground_truth,omitempty"`
// Workload is the incident's affected workload (namespace + name). It seeds the
// request and — when a catalog fixture is present — drives the recall structural
// gate (resource agreement). Optional; zero for alerts without a workload.
Workload *CaseWorkload `yaml:"workload,omitempty"`
// CatalogDir, when set, points at a directory of knowledge-base markdown entries
// RELATIVE to the case file. Its presence seeds an instant-recall catalog for this
// case and wires Recall + the adversarial verify pass into the replay loop exactly
// as production does — so the closed recall→verify loop is exercised mechanically in
// the replay eval. Absent ⇒ the case replays with no recall, unchanged.
CatalogDir string `yaml:"catalog_dir,omitempty"`
// Recall optionally tunes the recall gates for this case (mirrors config
// instant_recall). Absent (or a zero field) ⇒ the production default. Consulted only
// when CatalogDir is set.
Recall *CaseRecall `yaml:"recall,omitempty"`
// ExpectRecall asserts the recall outcome mechanically and fails the case when unmet:
// short_circuit — recall fired and its answer was delivered (loop skipped)
// withdrawn — recall fired but the verify pass rejected it and the loop fell
// through to a full investigation
// fired — recall fired (either short_circuit or withdrawn)
// rejected — a recall gate rejected the hit: recall never fired
// Empty ⇒ no recall assertion (existing cases are unaffected).
ExpectRecall string `yaml:"expect_recall,omitempty"`
// contains filtered or unexported fields
}
Case is one replayable incident.
func RecordedCase ¶
RecordedCase converts a live run's recorded tool calls into a replayable Case (the existing examples/eval format). v1 keeps the LAST output per tool: the replay staticTool returns one fixed output per tool regardless of args, so multi-call tools are flattened. submit_findings is excluded (it is the model's own output, not evidence).
type CaseAggregate ¶
type CaseAggregate struct {
Name string
Runs int
PassRate float64 // fraction of repeats whose Result.Pass is true
Reached bool // PassRate >= evalMinPassRate
Flaky bool // PassRate in (1-evalMinPassRate, evalMinPassRate): runs disagree
Confidence float64 // median confidence over repeats
Missing []string // union of missing keywords/entities across repeats
OverClaimed []string // union of over-claimed distractors across repeats
}
CaseAggregate is the k-of-n verdict for one case over N replay repeats.
type CaseRecall ¶ added in v0.7.0
type CaseRecall struct {
MinScore float64 `yaml:"min_score"`
MarginGap float64 `yaml:"margin_gap"`
SoloFloor float64 `yaml:"solo_floor"`
RequireWorkloadMatch bool `yaml:"require_workload_match"`
OutcomePrior float64 `yaml:"outcome_prior"`
OutcomeFloor float64 `yaml:"outcome_floor"`
}
CaseRecall tunes the recall gates for a replay case. A zero field takes the same production default as config.InstantRecall, so a case need only override what it must (e.g. a low solo_floor so a single-entry fixture fires deterministically).
type CaseWorkload ¶ added in v0.7.0
CaseWorkload is a case's affected workload for the request + recall structural gate.
type CompareCaseRow ¶ added in v0.3.0
type CompareCaseRow struct {
Name string `json:"name"`
Runs int `json:"runs"`
PassRate float64 `json:"pass_rate"`
Reached bool `json:"reached"`
Flaky bool `json:"flaky"`
Coverage float64 `json:"coverage"` // median coverage ratio over the case's runs
}
CompareCaseRow is one case's k-of-n verdict for one model, in the comparison.
type CompareSpec ¶ added in v0.3.0
type CompareSpec struct {
Judge *JudgeSpec `yaml:"judge,omitempty"`
Models []ModelEntry `yaml:"models"`
}
CompareSpec is the multi-model benchmark description: the model entries to benchmark against the replay suite, and (optionally) the judge that grades every entry. Keeping the judge in the spec makes a published comparison self-describing — the judge disclosure travels with the results.
func LoadCompareSpec ¶ added in v0.3.0
func LoadCompareSpec(path string) (CompareSpec, error)
LoadCompareSpec reads and validates a comparison spec. Unknown keys are rejected so a typo (e.g. "pricess") fails loudly instead of silently skewing a published benchmark.
type ComparedCase ¶ added in v0.3.0
type ComparedCase struct {
Name string
Runs []ComparedRun
}
ComparedCase is all N runs of one case for one model entry.
type ComparedRun ¶ added in v0.3.0
ComparedRun is one replay run of one case for one model entry: the deterministic keyword score, the tool-call coverage, and (when the case carries ground truth and a judge is set) the judge's rubric verdict.
type ComparisonReport ¶ added in v0.3.0
type ComparisonReport struct {
At string `json:"at"`
N int `json:"n"` // runs per case
Judge string `json:"judge"` // judge model disclosure, e.g. "anthropic/claude-…"
Models []ModelComparison `json:"models"`
}
ComparisonReport is the aggregate of a multi-model benchmark run: one row per model entry, in spec order (comparisons should not reorder by score), plus the disclosure a published benchmark needs — N runs and the judge model identity.
func NewComparisonReport ¶ added in v0.3.0
func NewComparisonReport(at string, n int, judge string, models []ModelComparison) ComparisonReport
NewComparisonReport builds a report from already-aggregated model rows, preserving the given (spec) order.
func ParseComparisonReport ¶ added in v0.3.0
func ParseComparisonReport(b []byte) (ComparisonReport, error)
ParseComparisonReport reads a report back from its JSON form (baseline diffs, tests).
func (ComparisonReport) JSON ¶ added in v0.3.0
func (r ComparisonReport) JSON() ([]byte, error)
JSON renders the deterministic machine-readable comparison report.
func (ComparisonReport) Markdown ¶ added in v0.3.0
func (r ComparisonReport) Markdown() string
Markdown renders the human comparison report: a per-model summary table (rubric breakdown, pass rate, coverage, confident-wrong, tokens, optional cost) followed by a per-case pass-rate matrix. Deterministic: models keep spec order, cases sort by name.
type ComparisonRunner ¶ added in v0.3.0
type ComparisonRunner struct {
Model providers.ModelProvider // the entry under test (wrap with CountingModel for token totals)
Judge Judge // fixed across entries; nil skips rubric grading
Log *slog.Logger
}
ComparisonRunner benchmarks one model entry over the replay cases. It mirrors the replay Runner (static tools, same loop) but additionally records tool calls for coverage and grades every run with a fixed judge, so entries can be compared on the full rubric — not only keyword pass/fail.
func (*ComparisonRunner) RunCases ¶ added in v0.3.0
func (cr *ComparisonRunner) RunCases(ctx context.Context, cases []Case, n int) []ComparedCase
RunCases replays every case n times against the entry's model.
type CountingModel ¶ added in v0.3.0
type CountingModel struct {
Inner providers.ModelProvider
// contains filtered or unexported fields
}
CountingModel wraps a ModelProvider and sums the provider-reported token usage across completions. The loop only logs/meters each response's Usage, so this wrapper is what turns per-response usage into a per-benchmark total.
func (*CountingModel) Complete ¶ added in v0.3.0
func (c *CountingModel) Complete(ctx context.Context, req providers.CompletionRequest) (providers.CompletionResponse, error)
Complete delegates to Inner and accumulates the response usage on success.
func (*CountingModel) Total ¶ added in v0.3.0
func (c *CountingModel) Total() providers.Usage
Total returns the usage accumulated so far.
type Coverage ¶
type Coverage struct {
Touched []string // mandatory source groups actually exercised
Missing []string // mandatory groups never touched
Bonus []string // optional groups touched
CrossSignal bool // >=2 distinct source groups exercised
ToolErrors []string // distinct tool names that returned an error
Ratio float64 // |touched| / |expected| (1.0 when no expected sources)
}
Coverage is the deterministic data-source coverage result for one run.
func ScoreCoverage ¶
ScoreCoverage computes coverage of the mandatory expected sources from the recorded calls. optional sources count as Bonus and never affect Ratio.
type Expected ¶
type Expected struct {
MustContain []string `yaml:"must_contain"` // keywords that must appear in the findings (recall, over full findings text)
MinConfidence float64 `yaml:"min_confidence"` // confidence floor (0 = no floor)
RootCauseEntities []string `yaml:"root_cause_entities"` // entities that MUST be named as the cause (entity recall, over claim text)
Distractors []string `yaml:"distractors"` // plausible-but-wrong entities that must NOT be blamed (over-claim/FP); only evaluated when root_cause_entities is non-empty
}
Expected is the RCA scoring spec for a case.
type GroundTruth ¶
type GroundTruth struct {
RootCause string `yaml:"root_cause"`
ExpectedSources []string `yaml:"expected_sources"` // MANDATORY data-source groups -> coverage gate
OptionalSources []string `yaml:"optional_sources"` // bonus if touched, never gates
ExpectedAction string `yaml:"expected_action"`
MustReachRoot bool `yaml:"must_reach_root"`
}
GroundTruth is the human-authored truth a scenario is graded against.
type Judge ¶
type Judge interface {
Grade(ctx context.Context, scn Scenario, inv providers.Investigation) (Verdict, error)
}
Judge grades an investigation against a scenario's ground truth.
type JudgeSpec ¶ added in v0.3.0
type JudgeSpec struct {
Provider string `yaml:"provider"`
BaseURL string `yaml:"base_url"`
Model string `yaml:"model"`
APIKeyEnv string `yaml:"api_key_env"`
}
JudgeSpec identifies the (single, fixed) judge model used for every entry. Grading is blind: the judge never sees which entry produced an investigation.
type LiveReport ¶
type LiveReport struct {
At string `json:"at"`
N int `json:"n"` // runs per scenario (live mode); 0 in replay mode
Ran int `json:"ran"` // scenarios actually investigated (not skipped)
Passed int `json:"passed"`
Skipped int `json:"skipped"`
Results []LiveResult `json:"results"`
}
LiveReport is the serializable output of one live-fire campaign run.
func NewLiveReport ¶
func NewLiveReport(at string, n int, results []LiveResult) LiveReport
NewLiveReport tallies results into a report.
func (LiveReport) JSON ¶
func (rep LiveReport) JSON() []byte
JSON is the machine-readable sibling of the markdown report.
func (LiveReport) Markdown ¶
func (rep LiveReport) Markdown() string
Markdown renders the human report: summary, per-scenario table, coverage heatmap.
func (LiveReport) RegressionsVS ¶
func (rep LiveReport) RegressionsVS(prev LiveReport) []string
RegressionsVS returns scenarios that passed in prev but fail/skip now.
type LiveResult ¶
type LiveResult struct {
Scenario string
Skipped bool
SkipReason string
Runs []RunOutcome
CoverageRatio float64 // median
DimMedian map[string]int // median per rubric dimension
DimVariance map[string]float64
ConfidentWrong bool // any run confident-wrong
Flaky bool // root_cause scores vary too much across runs to trust
ToolErrors []string // union across runs
Pass bool
}
LiveResult aggregates the N runs of one scenario.
type LiveRunner ¶
type LiveRunner struct {
Model providers.ModelProvider
BaseTools []investigate.Tool
Judge Judge
Steps StepRunner
Log *slog.Logger
N int // runs per scenario (default 1 if 0)
OnRecord func(Scenario, []Call) // optional: persist the run's calls (replay corpus)
Recall *investigate.Recall // optional; when set, runOnce takes the instant-recall short-circuit (production path). nil ⇒ no recall.
}
LiveRunner runs scenarios against real tools, grading coverage + RCA quality. BaseTools and Model are the LIVE tools/model (built by cmd/lore via buildModelAndTools); Judge uses a separate, stronger model.
func (*LiveRunner) RunScenario ¶
func (lr *LiveRunner) RunScenario(ctx context.Context, scn Scenario) LiveResult
RunScenario runs setup (or precheck), N investigations, judging each, then always tears down. Pass gate: at least evalMinPassRate of runs reach root_cause >= evalRootCauseBar, coverage median == 1.0, no confident-wrong run, and root_cause variance within evalMaxRootCauseVariance (not flaky).
type ModelComparison ¶ added in v0.3.0
type ModelComparison struct {
Name string `json:"name"` // the entry's report label
Provider string `json:"provider"`
Model string `json:"model"`
Effort string `json:"effort,omitempty"`
// Aggregate scores.
PassRate float64 `json:"pass_rate"` // fraction of cases that reached the k-of-n bar
Reached int `json:"reached"` // cases whose pass-rate met the k-of-n bar
Total int `json:"total"` // cases run
RubricMedian map[string]float64 `json:"rubric_median,omitempty"` // per-dimension median over graded runs; nil when ungraded
GradedRuns int `json:"graded_runs"` // runs the judge graded (0 ⇒ rubric omitted)
CoverageMedian float64 `json:"coverage_median"` // median coverage ratio over all runs
ConfidentWrong int `json:"confident_wrong"` // graded runs flagged confident-and-wrong
// Usage + cost.
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CostUSD *float64 `json:"cost_usd,omitempty"` // present only when the entry supplied prices
Cases []CompareCaseRow `json:"cases"`
}
ModelComparison is one model entry's aggregated result across all cases.
func AggregateModel ¶ added in v0.3.0
func AggregateModel(entry ModelEntry, cases []ComparedCase, usage providers.Usage) ModelComparison
AggregateModel folds a model entry's per-case replay runs into one comparison row. Per-case pass uses the same k-of-n bar as the single-run campaign; rubric medians and confident-wrong are computed over every graded run; coverage is the median over every run; cost is computed only when the entry supplied prices. Case rows come out sorted by name so two reports diff cleanly.
type ModelEntry ¶ added in v0.3.0
type ModelEntry struct {
Name string `yaml:"name"` // report label; must be unique
Provider string `yaml:"provider"` // "openai" (default) | "anthropic" | "gemini"
BaseURL string `yaml:"base_url"`
Model string `yaml:"model"`
APIKeyEnv string `yaml:"api_key_env"` // env var holding the API key (empty = keyless)
// Effort is the OpenAI-compatible reasoning_effort request field (e.g. "low",
// "medium", "high"). Only valid for the openai provider; other providers reject it.
Effort string `yaml:"effort,omitempty"`
// Prices enables the estimated-cost column; omit it to omit the column.
Prices *Prices `yaml:"prices,omitempty"`
}
ModelEntry is one model configuration to benchmark.
type ModelJudge ¶
type ModelJudge struct {
Model providers.ModelProvider
}
ModelJudge grades with an LLM (use a stronger model than the one under test).
func (ModelJudge) Grade ¶
func (j ModelJudge) Grade(ctx context.Context, scn Scenario, inv providers.Investigation) (Verdict, error)
Grade builds a blind grading prompt and parses the JSON verdict.
type Prices ¶ added in v0.3.0
type Prices struct {
InputUSD float64 `yaml:"input_usd" json:"input_usd"`
OutputUSD float64 `yaml:"output_usd" json:"output_usd"`
}
Prices is optional per-million-token (MTok) pricing for cost estimation.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects tool calls made during one investigation run. Safe for concurrent use (the loop is sequential today, but tools may fan out later).
type Result ¶
type Result struct {
Name string
Pass bool
Confidence float64
Missing []string // expected keywords/entities not found (or an error note); includes "over-claimed: <e>" markers
OverClaimed []string // distractor entities the investigation wrongly blamed (over-claim/FP)
// Recall telemetry (populated only for cases with a catalog fixture): whether
// instant recall fired, and whether its answer short-circuited the loop. Surfaced
// so recall behaviour is asserted mechanically, not inferred from the finding.
RecallFired bool
RecallShortCircuit bool
}
Result is the score for one case.
func Score ¶
func Score(name string, inv providers.Investigation, exp Expected) Result
Score reports whether the investigation identifies the expected root cause. Keyword recall (must_contain) is matched over the full findings text. Entity scoring — recall over root_cause_entities and an over-claim penalty over distractors — is matched over the CLAIM text only (what was blamed), and engages only when root_cause_entities is set. A case passes when nothing is missing, no distractor was blamed, and confidence meets the floor.
type RunOutcome ¶
type RunOutcome struct {
Investigation providers.Investigation
Coverage Coverage
Verdict Verdict
}
RunOutcome is one of the N runs of a scenario.
type Runner ¶
type Runner struct {
Model providers.ModelProvider
Log *slog.Logger
}
Runner replays cases through the investigation loop with a given model.
type Scenario ¶
type Scenario struct {
ID string `yaml:"id"`
Category string `yaml:"category"` // what-changed | saturation | network | cloud | dependency | cert | dns | storage | instant-recall
Description string `yaml:"description"`
Invasive bool `yaml:"invasive"` // true => has setup/teardown; false => natural failure
Precheck string `yaml:"precheck"` // optional shell; non-zero exit => SKIP (natural scenarios)
Setup []string `yaml:"setup"` // shell steps (kubectl/flux) to induce the fault
Trigger Trigger `yaml:"trigger"`
GroundTruth GroundTruth `yaml:"ground_truth"`
Teardown []string `yaml:"teardown"` // shell steps to revert; always run
}
Scenario is one live-fire eval case: an induced-or-natural failure, how to trigger an investigation, and the ground truth to grade against. It is a superset of Case (which is the recorded/replay form this produces).
func LoadScenarios ¶
LoadScenarios reads every *.yaml / *.yml scenario in dir.
type StepRunner ¶
StepRunner executes a scenario's shell setup/teardown/precheck steps. The real implementation shells out (kubectl/flux); tests use a fake.
type Trigger ¶
type Trigger struct {
Mode string `yaml:"mode"` // "cli" (default) | "webhook"
Symptom string `yaml:"symptom"` // free-text incident description
Namespace string `yaml:"namespace"` // affected namespace (optional)
}
Trigger describes how the investigation is started.