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 NewStaticTool(name, output string) investigate.Tool
- 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 ¶
func GateError ¶
GateError returns a non-nil error when the campaign pass-rate is below failUnder (which is only enforced when failUnder > 0). The message names the cases that did not reach RCA and any flaky cases, so CI logs explain the failure.
func NewStaticTool ¶ added in v0.9.0
func NewStaticTool(name, output string) investigate.Tool
NewStaticTool exposes the replay tool (fixed recorded output regardless of args) for reuse OUTSIDE the eval package — notably the `lore demo investigate` command, which drives the real loop against these fakes with no cluster. Additive: it does not change how runOne constructs its own staticTools.
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).
func (Case) AffectedWorkload ¶ added in v0.9.0
AffectedWorkload returns the case's affected workload (zero when unset), so the demo can seed the Request.Workload exactly as runOne does.
func (Case) DisplayName ¶ added in v0.9.0
DisplayName returns the case's name for demo/report labeling.
func (Case) FakeTools ¶ added in v0.9.0
func (c Case) FakeTools() []investigate.Tool
FakeTools returns the case's recorded evidence as replay tools (one per entry in c.Tools), the same fakes runOne wires into the loop. Exposed additively so the demo command can build the identical zero-cluster tool set from a fixture.
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, transcript ...string) (Verdict, error)
}
Judge grades an investigation against a scenario's ground truth. An optional tool-transcript excerpt may be supplied so the judge can check that cited evidence traces to a real tool result (groundedness) rather than grading the findings text in isolation; callers with no transcript omit it.
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, transcript ...string) (Verdict, error)
Grade builds a blind grading prompt and parses the JSON verdict. An optional transcript excerpt (first variadic arg) is appended, bounded, so the judge can check evidence groundedness against the tool results the investigation saw.
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.