ek.agents.harness#
The agent task-suite harness: k trials, per-slice cuts, and a gate that survives noise.
run_suite() is ek.evaluate_store() specialized for agents: it runs the system under
test over a suite k times per task (reliability needs repeats), grades each episode with an
injected checker, and reports pass^k + cost + latency per slice.
Why a separate gate. ek’s offline ek.regression_gate() is a scalar point
comparison with tolerance=0.0 – correct for a deterministic OCR benchmark, unsound for
an agent. Agent metrics are random variables: re-running the same agent moves the number. A
point gate therefore (a) flags sampling noise as a regression and (b) misses a real regression
that hides inside the tolerance band.
agent_regression_gate() instead tests the difference current - baseline with both
runs’ uncertainty folded in (a Newcombe score interval on the success rate; a combined-SE bound
on pass^k), failing only when the upper bound of that change is confidently negative. Two
tempting shortcuts are both wrong, and this module has shipped and fixed each of them:
comparing our interval to the baseline’s point (the baseline is itself noisy, so one flake
against a lucky baseline reads as a regression), and asking whether two 95% intervals overlap
(that is a ~0.5% test, not a 5% one – it goes blind to a 15-point drop on a 60-task suite).
An empty run, and an empty baseline, are both refused: with no trials every bound is the
maximally-uncertain default, so nothing could ever be shown to fall below it.
Hidden eval variables. In a tau-bench-style suite the user simulator is itself an LLM –
change its model or prompt and the scores move, silently. So a baseline records its
RunProvenance and the gate refuses to compare across a changed
simulator, suite version, or scaffold, exactly as the offline gate already refuses to compare
two different metrics. Comparing across them is not a regression check, it is a category error.
Verifier isolation is a security property. The checker must run where the agent’s trajectory
cannot reach it (a red-team produced a ~10-line conftest.py that rewrote every test outcome
to “passed”). ek cannot enforce your sandbox – it only refuses to pretend the problem does
not exist.
Example
>>> from ek.agents.base import Cost, Episode, TaskSpec
>>> tasks = [TaskSpec("t1", input="2+2", gold="4"), TaskSpec("t2", input="3+3", gold="6")]
>>> def agent(task):
... answers = {"2+2": "4", "3+3": "7"} # gets t2 wrong, every time
... return Episode(output=answers[task.input], cost=Cost(input_tokens=10))
>>> report = run_suite(agent, tasks, k=1)
>>> report.success_rate, report.pass_hat_k
(0.5, 0.5)
Functions
|
Fail only when the evidence says the agent really got worse -- not on noise. |
|
Load a named agent baseline (or |
|
Run |
|
Freeze a run as a named baseline (including its provenance, so the gate can guard it). |
Classes
|
Outcome of a variance-aware agent regression check. |
- class ek.agents.harness.AgentGateResult(passed=True, reasons=<factory>, pass_hat_k=None, pass_hat_k_baseline=None, success_rate=None, success_rate_baseline=None, cost_per_success=None, cost_per_success_baseline=None, underpowered=False)[source]#
Bases:
objectOutcome of a variance-aware agent regression check.
- ek.agents.harness.agent_regression_gate(report, baseline, *, tolerance=0.0, cost_tolerance=0.0, rootdir=None)[source]#
Fail only when the evidence says the agent really got worse – not on noise.
Tests the difference
current - baseline, with both runs’ uncertainty folded in: a Newcombe score interval on the success rate, and (fork > 1) a combined-SE bound onpass^k. A regression is declared only when the upper bound of that change is below-tolerance– i.e. we are confident the true change is negative.Two tempting shortcuts are both wrong, and this module has shipped and fixed each:
Comparing our interval to the baseline’s point. The baseline is itself a noisy estimate; a Wilson upper bound only reaches 1.0 when every trial passed, so a single flake against a lucky baseline reads as a “confident regression”.
Asking whether the two 95% intervals overlap. That is not a 5% test but roughly a 0.5% one: it buys a calm gate by going blind (a 15-point drop on a 60-task suite becomes invisible). A gate that cannot see a real regression is as broken as one that cries wolf.
A result that is merely worse but not significantly so is not a clean pass either – it is an underpowered experiment, and the result says so (
underpowered, plus a warning).- Raises:
ValueError – if the baseline was produced with a different user simulator, suite version, or scaffold – those runs are not comparable, and silently comparing them would be a category error (the same reason the offline gate refuses to compare two different metrics); if the baseline recorded zero trials (a permanent free pass); or if it is malformed.
- Return type:
- ek.agents.harness.load_agent_baseline(name, *, rootdir=None)[source]#
Load a named agent baseline (or
None).
- ek.agents.harness.run_suite(agent, tasks, *, k=1, check=None, metrics=None, price=None, prices=None, run=None, seed=None, persist=False, run_id=None, rootdir=None)[source]#
Run
agentover a task suite,ktrials per task, and report reliability + cost.- Parameters:
agent (
Callable[[TaskSpec],Any]) – The system under test:TaskSpec -> Episode(a bare answer is wrapped). It receives the whole spec –.input,.tools,.gold– not just the input. If it accepts a keywordseed, the per-trial seed is passed to it.k (
int) – Trials per task.k > 1is what makespass^kmeaningful – and it only means anything if the agent is genuinely stochastic (the report warns if it is not).check (
Any) – The success oracle – a registered checker name or an(episode, gold) -> boolcallable. Defaults to"output"; inject a state-based/executable oracle for a real suite, and run it isolated from the agent.metrics (
Optional[Mapping]) – Optional{name: Metric}scored on every episode againsttask.gold, with the suite’s Layer-A grammar injected – this is how the tool/argument cost weights reach atool_call/trajectorymetric. Results land inreport.detail["metrics"].price/prices – Rates for costing the episodes (see
ek.agents.cost).run (
Optional[RunProvenance]) –RunProvenancefor this run (model, simulator, suite version, scaffold). Recorded on every episode and on any baseline saved from it.seed (
Optional[int]) – Base RNG seed. Trial i of each task usesseed + i: it is recorded on the episode’s provenance and passed to the agent when the agent accepts aseedkeyword.persist/run_id/rootdir – Persist the run to the
runs/resultsstores.
- Return type:
- Returns:
A
ReliabilityReportcarryingpass@k,pass^k(with a bootstrap CI), the success rate (with a Wilson CI), the per-slice cuts, and the cost report – because reliability without cost is only half the answer.
- ek.agents.harness.save_agent_baseline(report, name, *, rootdir=None)[source]#
Freeze a run as a named baseline (including its provenance, so the gate can guard it).
- Raises:
ValueError – if the run evaluated nothing. An empty baseline is worse than no baseline – it is a permanent free pass, because every bound in it is the maximally-uncertain default and no later run can be shown to fall below it.
- Return type: