Spectre's evaluation tools measure two different properties together:
- whether the router selected an expected route or outcome;
- whether it used the LLM classifier only when the case allowed or required it.
This catches a regression that route-only accuracy misses: a route can be correct while still making an unnecessary model call.
Run A Corpus
Store one JSON object per line:
{"id":"deterministic-help","input":"help","expected_route":"HELP","expected_strategy":"regex","llm":"forbidden","tags":["deterministic","english"]}
{"id":"local-help","input":"show me the available commands","expected_route":"HELP","llm":"forbidden","tags":["local","english"]}
{"id":"ambiguous-billing","input":"something is wrong with the invoice for my project","allowed_routes":["BILLING","PROJECT_SUPPORT"],"llm":"required","tags":["ambiguous","english"]}Then run:
mix spectre.eval MyApp.Agent test/fixtures/routing.jsonl
The equivalent --agent form is:
mix spectre.eval --agent MyApp.Agent test/fixtures/routing.jsonl
The summary includes case pass rate, route accuracy, LLM policy violations, outcomes, strategy usage, and p50/p95 routing duration.
Case Fields
idis a required stable case identifier.inputis a required string or Spectre input-shaped object.expected_outcomeisroute,clarify,unknown, orerror; it defaults toroute.expected_routeis one accepted label.allowed_routesadds other accepted labels for genuinely ambiguous cases.expected_strategyoptionally fixes the expected winning provider.llmisforbidden,allowed, orrequired; it defaults toallowed.statesupplies an explicit state snapshot without loading the configured state adapter. Existing flow names may be written as strings.tagsgroups cases for report analysis.max_duration_usoptionally sets a per-case duration ceiling. Avoid tight ceilings for network-backed providers.
Blank lines and lines beginning with # are ignored. Invalid JSON or invalid
fields report the exact source line.
docs/examples/routing-eval.jsonl is a small
starter corpus. Its labels and expectations are illustrative and should be
adapted to the agent under evaluation.
CI Thresholds And JSON Artifacts
By default, every case must pass and no forbidden or required LLM policy may be violated:
mix spectre.eval MyApp.Agent test/fixtures/routing.jsonl \
--json tmp/spectre-routing.json
Thresholds can be relaxed explicitly while a corpus is being calibrated:
mix spectre.eval MyApp.Agent test/fixtures/routing.jsonl \
--min-pass-rate 0.98 \
--max-unnecessary-llm 1 \
--max-missing-llm 0 \
--max-errors 0
The task exits unsuccessfully when thresholds are not met, so it can be used as a CI regression gate. The JSON artifact contains privacy-safe receipts and structured violations for every case.
Programmatic API
Evaluate a complete corpus:
{:ok, report} =
Spectre.Eval.run(
MyApp.Agent,
"test/fixtures/routing.jsonl",
router_opts: [tenant_id: "evaluation"]
)
if Spectre.Eval.Report.acceptable?(report) do
:ok
else
{:error, report}
endEvaluate one route:
{:ok, receipt} =
Spectre.Router.evaluate(
MyApp.Agent,
"show me my invoices",
state: %Spectre.State{current_flow: :billing}
)
receipt.label
receipt.strategy
receipt.llm_called?
receipt.attemptsSpectre.Router.Receipt excludes input text, prompts, model output, candidate
matches, and route handlers. It contains the outcome, winning label and
strategy, sanitized provider attempts and candidates, total duration, and a
provider_calls list with the normalized outcome and duration of every shared
provider-boundary invocation. llm_called? becomes true only when an LLM
adapter worker was actually invoked; selecting LLM arbitration before prompt
construction is not counted as model use.
A provider-call entry has only operational metadata:
%{
provider: :llm,
purpose: :classifier,
outcome: :ok,
duration_us: 12_430,
invoked?: true
}Configuration rejected before a worker starts is recorded with
invoked?: false. Raw provider errors and responses are never copied into this
list.
Execution Boundary
Evaluation runs the configured input and router pipelines because testing a different path would give misleading results. It does not:
- load or persist through the state adapter;
- load or write memory;
- run the selected route handler;
- plan or execute actions;
- deliver journal records;
- write online semantic-learning examples.
Classifier, embedding, semantic-cache lookup, and LLM adapters are router providers and therefore may be called. A real LLM evaluation can incur latency and provider charges. CI should normally use deterministic adapter fixtures; run a separate opt-in corpus when measuring a live provider.
The receipt is intended to become the common fact source for evaluation, journaling, and minimal telemetry. It does not calculate monetary token cost or export metrics.