Evaluation and Release Gates
AgenticLens evaluates agent behavior from local outputs and structured traces. The evaluation layer converts workflow evidence into repeatable quality, tool-use, latency, and cost checks. It can run without a hosted service or model provider.
Evaluation Model
An evaluation uses two inputs:
- a versioned YAML or JSON test suite containing acceptance criteria
- samples containing the observed output and its AgenticLens trace
Each test case can define:
- exact output or required output phrases
- required and forbidden tool calls
- required tool arguments
- expected JSON output structure and required fields
- maximum turn count
- maximum end-to-end latency
- maximum estimated cost
- tags and application-specific metadata
Built-in checks are deterministic. Custom evaluators and LLM-as-a-Judge evaluators use the same normalized score contract. A case passes only when every required check reaches its configured threshold. The report includes per-check evidence, aggregate pass rate, average score, latency, and total cost.
Unified Evaluator Framework
The evaluator framework is provider-neutral. An evaluator receives an
EvaluationContext containing the test case, observed sample, trace, threshold,
and evaluator-specific configuration. It returns one or more normalized
Score objects with a value from 0.0 to 1.0, an explanation, and optional
evidence metadata.
Trusted evaluators are registered explicitly in application code:
from agenticlens.evaluation import (
EvaluationContext,
EvaluatorRegistry,
LLMJudgeEvaluator,
Score,
evaluate_suite,
)
def judge(context: EvaluationContext) -> Score:
# Call any hosted model, local model, or internal evaluation service here.
result = call_model(
input=context.case.input,
output=context.sample.output,
rubric=context.config.config["rubric"],
)
return Score(
name="answer_quality",
value=result.score,
passed=False, # AgenticLens applies the suite threshold.
explanation=result.explanation,
metadata={"model": result.model},
)
registry = EvaluatorRegistry()
registry.register(LLMJudgeEvaluator("quality_judge", judge))
report = evaluate_suite(suite, samples, registry=registry)
The suite selects the evaluator and controls its acceptance threshold:
cases:
- id: support-answer
name: Support answer quality
evaluators:
- name: quality_judge
threshold: 0.80
required: true
config:
rubric: The answer must be correct, grounded, and concise.
Suite files never import or execute Python modules. Registration is performed by trusted application code, preventing untrusted suite configuration from loading arbitrary code.
CallableEvaluator supports Python rules, semantic similarity functions,
safety classifiers, RAG metrics, and internal services.
LLMJudgeEvaluator identifies provider-supplied LLM-as-a-Judge results in reports
while leaving model selection, credentials, prompts, retries, and structured
output handling under application control. See examples/custom_llm_judge.py
for a complete, runnable registration example.
BusinessRuleEvaluator is a named wrapper around trusted application logic for
organization-specific pass/fail rules that do not need an LLM-as-a-Judge
evaluator.
Run an Evaluation
agenticlens evaluate suite.yaml samples.json \
--save evaluation.json \
--html evaluation.html
The JSON report is suitable for CI and further analysis. The standalone HTML report is suitable for demonstrations, release reviews, and team sharing.
Versioned Evaluation Datasets
AgenticLens can also store evaluation samples as a versioned dataset artifact that carries split assignments, tags, and human labels for judge calibration.
Summarize a dataset:
agenticlens dataset summary dataset.json
Assign deterministic train, validation, and test splits:
agenticlens dataset split dataset.json \
--save dataset-split.json \
--train-ratio 0.7 \
--validation-ratio 0.15 \
--test-ratio 0.15 \
--seed 7
Export a split back to a plain samples.json artifact for evaluation:
agenticlens dataset export-samples dataset-split.json \
--split test \
--save samples-test.json
See examples/dataset_and_calibration_demo.py for a complete local example that
creates a dataset from samples, assigns splits, runs evaluation, and calibrates
judge labels end to end.
Run a Trusted Live Target
evaluate-live runs the same suite against a trusted live target instead of a
pre-recorded sample file.
Run a Python callable:
agenticlens evaluate-live suite.yaml \
--target-kind python \
--target examples/live_evaluation_demo.py:run_case \
--save evaluation-live.json
Run an HTTP target:
agenticlens evaluate-live suite.yaml \
--target-kind http \
--target http://localhost:8000/evaluate \
--save evaluation-live.json
Add --max-workers 4 to call the target for several cases at once, and
--html report.html for a standalone report. Case order in the report is the
suite's order regardless of completion order. Only raise --max-workers for
targets that are safe to call concurrently. With the default of one worker,
each result is validated before the next case is sent, so an invalid payload
stops the run without further target calls.
Python and HTTP targets share one scoring path. tests/test_live_parity.py
runs one suite against both (with AgenticLens Run-shaped traces) and checks
that the reports are identical, sequentially and concurrently.
Live targets are intentionally powerful developer-facing integrations. Python targets execute local code and HTTP targets can reach arbitrary URLs, so suite files and target definitions should be treated as trusted inputs.
Evidence Behind Every Score
Every score in a report states what produced it:
| Score source | evaluator |
evaluator_version |
|---|---|---|
| Built-in checks | agentic_evals.builtin |
the installed agentic-evals version |
| Registered evaluator | its registry name | its declared version= (or null) |
Version a judge whenever its prompt or model changes:
LLMJudgeEvaluator("answer_quality", judge, version="quality-prompt-v3/gpt-4o-2024-08-06")
Each case also records cost_source (measured or estimated) and
sample_available. AgenticLens run costs are estimated (token counts times a
price table) unless the run's metadata declares cost_source: measured.
audit_report(report) (and agenticlens evaluate --audit audit.json) checks:
| Issue | Blocking by default |
|---|---|
| A score without an evaluator, or an evaluator without a version | yes (require_evaluator_versions=False to relax) |
| A failed case with no sample, or a sample without a trace ID | always |
| Not every case has a cost | no (require_complete_cost=True) |
| A priced case without measured/estimated provenance | no (require_cost_source=True) |
agenticlens evaluate ... --strict-evidence exits with code 3 on blocking
issues. The HTML report shows evaluator@version beside each score, the cost
source beside each cost, and "no sample supplied" for missing samples.
Example Advanced Checks
cases:
- id: support-answer
name: Structured support answer
input:
question: Where is my refund?
required_tools: ["lookup_refund"]
required_tool_arguments:
lookup_refund: ["order_id"]
output_json_schema:
type: object
required: ["answer", "meta"]
properties:
answer:
type: string
meta:
type: object
required: ["confidence"]
required_output_fields:
- meta.confidence
max_turns: 3
max_latency_ms: 1200
max_cost_usd: 0.02
Apply a Release Gate
agenticlens gate evaluation.json \
--min-pass-rate 0.95 \
--min-average-score 0.98 \
--max-failed-cases 1 \
--max-average-latency-ms 1500 \
--max-total-cost-usd 0.25
The command exits with status 0 when every threshold passes, 2 when the
release gate fails, and 1 when the report or configuration is invalid.
Alerting on gate failures
agenticlens gate evaluation.json --min-pass-rate 0.95 --max-total-cost-usd 0.25 \
--notify-webhook "$WEBHOOK_URL" \
--notify-format slack \
--cooldown-minutes 60 \
--notify-state .agenticlens/gate-notifications.json
--notify-webhook can also come from AGENTICLENS_GATE_WEBHOOK. When the gate
fails, AgenticLens works out which configured thresholds broke (keys such as
pass_rate, average_latency_ms, total_cost_usd, tag_pass_rate:<tag>,
metric_pass_rate:<metric>) and sends one alert covering those not in
cooldown. The state file records when each key last alerted; cache it between
CI runs to keep cooldowns across jobs. Formats:
generic:{"event": "agenticlens.gate.failed", "suite_name", "reasons", "breaches", "observed", ...}slack: an incoming-webhook{"text": ...}messageteams: aMessageCard
An alert that cannot be delivered is reported as a warning; the gate's exit
code is unchanged. In Python, notify_gate_breach() also accepts per-key or
per-category (quality, latency, budget) cooldowns, for example a short
cooldown for budget breaches and a long one for quality.
max_turns requires trace.metadata.turn_count to be recorded as a positive
integer. AgenticLens does not infer conversational turns from lower-level span
types when that metadata is absent.
Judge Calibration
Given a versioned, human-labeled reference set (a CalibrationDataset: a flat
list of {case_id, passed} reference judgments, distinct from the richer
EvaluationDataset records used by agenticlens dataset), AgenticLens can
compare one llm_judge evaluator's saved verdicts against those labels and
report:
- agreement rate with a 95% Wilson confidence interval
- true/false accept and true/false reject confusion counts
- trace-linked evidence for every compared case
agenticlens calibrate evaluation.json labels.json \
--evaluator answer_quality \
--save calibration.json
Case ids in the reference set must exactly match the report's case ids, and
the report's suite name/version must match the reference set's — a mismatch
raises rather than silently comparing misaligned evidence. Uses each case's
already-recorded Score.passed decision, not a re-derived 0.5 threshold.
Tuning the judge's pass threshold
agenticlens calibrate evaluation.json labels.json --evaluator answer_quality \
--save calibration.json --tune-threshold tuning.json
tune_judge_threshold() re-decides judge_value >= threshold for each
candidate threshold (every distinct judge value plus 0 and 1 by default) and
recommends the one with the best balanced accuracy — or raw agreement when the
labels contain only one class. It is fitted on the same labels it is scored on;
confirm it on a held-out split (agenticlens dataset split) before adopting it.
Structured Judge Verdicts
structured_judge() builds an LLMJudgeEvaluator from a function returning a
JudgeVerdict:
| Field | Meaning |
|---|---|
verdict |
agree, partially_agree, or disagree (scored 1.0 / 0.5 / 0.0 by default) |
confidence |
the judge's confidence, recorded but not multiplied into the score |
unsupported_claims |
claims in the output the evidence does not support |
evidence_missed |
evidence the output should have used |
rationale |
free-text reasoning |
The verdict, judge model, evaluated model, and a self_confirmation_risk flag
are stored on the score's metadata; judge_verdict(score) reads them back.
The evaluated model comes from case metadata, then trace metadata
(evaluated_model, model, or model_name). If it equals judge_model, the
case is scored as skipped — the judge would be grading its own output — unless
allow_self_judging=True. See examples/structured_judge_demo.py.
Judge identity, prompt version, and cost
structured_judge() also records judge_provider, prompt_version, and the
SHA-256 of prompt_template on every score, so an edited judge prompt shows up
in saved reports. The judge call's wall-clock latency is measured
(judge_latency_ms). When the judge reports what its call consumed through
JudgeVerdict.usage (JudgeUsage(cost_usd=..., cost_source="measured",
input_tokens=..., output_tokens=...)), the score records judge_cost_usd,
judge_cost_source, and token counts. Any other judge can set the same
metadata keys itself. Without a cost, analyze_scores() estimates one from
tokens and AgenticLens pricing when judge_provider and the judge model are
known, and labels it estimated.
evaluator = structured_judge(
"answer_quality",
my_judge, # returns JudgeVerdict(..., usage=JudgeUsage(...))
judge_model="claude-haiku-5-5",
judge_provider="anthropic",
version="1.0",
prompt_version="answer-quality-v2",
prompt_template=PROMPT,
)
Experiments (repeated trials across variants, pass@k, significance tests) are covered in Experiments and Statistical Comparison.
Score Analysis: Deterministic vs Model-Based
analyze_scores(report) (and agenticlens evaluate --score-analysis PATH)
reports three things a single pass rate hides:
- Deterministic and model-based scores, separately. Scores from
llm_judge/llm_rubricevaluators (or carryingjudge_model) are model-based; everything else is deterministic. Each side gets its own pass rate, average, and evaluator list. - Judge cost, kept apart from the cost of the agent under evaluation: total
and per evaluator, priced versus unpriced judge scores, cost sources, tokens,
and latency.
completeis false when any judge score has no cost. - Evaluator disagreement. All deterministic checks on a case vote as one unit; each model-based evaluator votes on its own. Cases where the units disagree are listed (with each side's explanation), and pairwise agreement rates are reported per evaluator pair.
The evaluate CLI prints a one-line summary, and the HTML report adds a
"Score analysis" section whenever model-based scores are present.
schemas/evaluation/score-analysis.schema.json publishes the contract.
Trace-Aware Evaluators
These evaluators score what the trace recorded, not only the final text. They
are registered by name in agenticlens.evaluation.default_registry()
(alongside agentic-evals' built-in scorers), which evaluate and
evaluate-live use, so suites can reference them directly:
evaluators:
- name: tool_trajectory
config: {expected: [kb_search, lookup_order], mode: in_order}
- name: groundedness
threshold: 0.8
- name: safety
config: {allow: [support@example.com], forbidden_terms: [internal only]}
| Evaluator | Checks | Config |
|---|---|---|
tool_trajectory |
Recorded tool sequence vs. expected | expected, mode: exact / in_order / any_order |
agent_goal |
Task success, run not failed, no failed spans, final agent, required output text | require_task_success, allow_failed_spans, final_agent, goal_contains |
handoff |
Handoff count, allowed pairs, A→B→A bounces | max_handoffs, allowed ("a->b"), max_round_trips |
loop |
Longest streak of identical tool calls | max_repeats (default 2) |
groundedness |
Share of output sentences supported by retrieved documents | min_overlap (0.6), context |
relevance |
Share of query terms the output addresses | query (defaults to case input) |
completeness |
Share of required points mentioned | required_points (strings or lists of alternatives); falls back to expected_contains |
citation |
Citations resolve to retrieved document IDs | pattern, require_citations, known_sources |
safety |
Email, SSN, phone, Luhn-valid card numbers, and forbidden terms in the output and in tool arguments | allow, forbidden_terms, check_pii |
Retrieved documents come from context in the evaluator config or case
metadata, otherwise from retrieval spans (output_data, or documents /
retrieval.documents / chunks attributes). Span semantics reach evaluators
through namespaced EvalSpan attributes that to_eval_trace() adds
(agenticlens.span_type, agenticlens.agent_name, agenticlens.status,
agenticlens.input_fingerprint, agenticlens.retrieved), plus
agenticlens.run_status and agenticlens.task_success in trace metadata.
Lexical heuristics
groundedness, relevance, and completeness compare words, not meaning:
paraphrases count as unsupported and copied-but-wrong claims as supported.
Use them as cheap first-pass signals next to a structured LLM judge; the
score analysis then shows where they disagree. The safety evaluator never
copies matched values into the score, only their kind and location.
examples/advanced_evaluation_demo.py runs these evaluators and a costed
structured judge over three recorded RAG runs and saves the report, score
analysis, and HTML in examples/advanced_evaluation/artifacts/.
Offline Pitch Demonstration
The repository includes a deterministic LangGraph supervisor demonstration that produces a trace, evaluation JSON, release decision, and HTML report:
uv sync --extra langgraph
uv run python -m examples.pitch_demo.run_pitch_demo
Generated artifacts are written to examples/pitch_demo/artifacts/. The demo
performs real graph execution but does not require an API key or network access.
Scope
The framework supports deterministic checks, trace-aware evaluators, custom or model-based evaluators with recorded versions, structured judge verdicts with prompt versions and judge cost, score analysis separating deterministic and model-based results with evaluator disagreement, versioned local datasets, judge calibration and threshold tuning against human labels, evidence audits, concurrent live targets, and gate alerts. Failure diagnosis and attribution are covered in Diagnosis and Failure Attribution. Built-in provider clients, async evaluator APIs, and automatic framework event adapters remain future roadmap capabilities.