Skip to content

hylo-eval

Hylo evaluation engine: benchmark contract and splits, scorers, paired statistics and gate tests, evaluation runner, experiments

Repository hylo-harness, packages/hylo-eval. Licence: Apache-2.0. Depends on: hylo-core.

Registered entry points
Group Name Object
benchmarks testing/reach-v0 hylo.eval.builtin:testing_benchmark
gate_tests mcnemar hylo.eval.stats:McNemar
gate_tests paired_permutation hylo.eval.stats:PairedPermutation
gate_tests sign hylo.eval.stats:SignTest
scorers safety_interventions hylo.eval.scoring:SafetyInterventionScorer
scorers steps hylo.eval.scoring:StepsScorer
scorers success hylo.eval.scoring:SuccessScorer
Module Summary
hylo.eval Hylo benchmark layer
hylo.eval.benchmark Moved to hylo.core.benchmark (the benchmarks contract lives in hylo-core); kept as an alias
hylo.eval.builtin The test benchmark "testing/reach-v0" over the test double environment (hylo.testing): for unit tests
hylo.eval.conformance Conformance for gate-test plugins (hylo.eval.stats.GateTest)
hylo.eval.experiment Build components from an ExperimentConfig through the plugin registry and run it
hylo.eval.runner EvaluationRunner: runs a benchmark split through the kernel, then scores from the trace
hylo.eval.scoring Scorers read the trace only; they never touch the Agent or the Environment
hylo.eval.stats Gate tests (plugin group "gate_tests"): the paired statistic the improvement protocol uses to decide

hylo.eval

Hylo benchmark layer.

META module-attribute

META = {'contracts': {'benchmarks': 'hylo.benchmark/v0.1', 'scorers': 'hylo.scorer/v0.1', 'gate_tests': 'hylo.gate_test/v0.1'}, 'maturity': 'implemented', 'plugins': {'gate_tests/paired_permutation': {'maturity': 'unverified', 'notes': 'not yet run in a campaign'}, 'gate_tests/sign': {'maturity': 'unverified', 'notes': 'not yet run in a campaign'}}}

hylo.eval.benchmark

Moved to hylo.core.benchmark (the benchmarks contract lives in hylo-core); kept as an alias.

STANDARD_SPLITS module-attribute

STANDARD_SPLITS = ('train', 'validation', 'heldout')

RSI_SPLITS module-attribute

RSI_SPLITS = ('search', 'search_val', 'gate', 'heldout')

Benchmark

Bases: Contract

id instance-attribute

id: str

version instance-attribute

version: str

tasks instance-attribute

tasks: tuple[TaskSpec, ...]

splits instance-attribute

splits: dict[str, tuple[int, ...]]

requires_privileged class-attribute instance-attribute

requires_privileged: bool = False

metadata class-attribute instance-attribute

metadata: dict[str, str] = Field(default_factory=dict)

instances

instances(split: str) -> list[TaskInstance]

digest

digest() -> str

standard_splits

standard_splits(train: int, validation: int, heldout: int) -> dict[str, tuple[int, ...]]

Disjoint seed ranges: train 0.., validation 10000.., heldout 20000..

rsi_splits

rsi_splits(n_states: int = 50, bounds: tuple[int, int, int] = (20, 30, 40), replicates: int = 1, gate_looks: int = 1) -> dict[str, tuple[int, ...]]

RFC-0002 split of init-state indices: search (diagnosis), search_val (selection), gate (confirmation), heldout (report only). Replicate r reuses init state (seed % n_states) with new policy noise (seed + r * n_states). gate_looks > 1 adds gate_r1 .. gate_r{looks-1}: further noise replicates of the gate init states, evaluated only when an earlier look is inconclusive (group-sequential gate).

gate_split_names

gate_split_names(gate_looks: int) -> list[str]

hylo.eval.builtin

The test benchmark "testing/reach-v0" over the test double environment (hylo.testing): for unit tests only. Real benchmarks live in hylo-bench.

testing_benchmark

testing_benchmark(tags: list[str] | None = None, train: int = 10, validation: int = 20, heldout: int = 20, protocol: str = 'standard', gate_looks: int = 1) -> Benchmark

hylo.eval.conformance

Conformance for gate-test plugins (hylo.eval.stats.GateTest).

check_gate_test

check_gate_test(test: Any, *, trials: int = 200) -> ConformanceReport

hylo.eval.experiment

Build components from an ExperimentConfig through the plugin registry and run it.

Only core and eval are imported here; envs, agents and memory backends arrive as plugins.

env_factory

env_factory(cfg: ExperimentConfig) -> Callable[[], Environment]

build_agent

build_agent(cfg: ExperimentConfig) -> Agent

run_experiment

run_experiment(cfg: ExperimentConfig, *, split: str | None = None, run_root: str | Path | None = None, label: str | None = None, agent: Agent | None = None, progress: Callable[[dict], None] | None = None) -> EvalResult

hylo.eval.runner

EvaluationRunner: runs a benchmark split through the kernel, then scores from the trace.

Sequential in v0.1. Sharding / resume follow the vla-eval SQLite work-queue pattern later.

EVALLOG_VERSION module-attribute

EVALLOG_VERSION = 'hylo.evallog/v0.1'

EvalResult dataclass

EvalResult(run_dir: Path, log: dict[str, Any])

run_dir instance-attribute

run_dir: Path

log instance-attribute

log: dict[str, Any]

metrics property

metrics: dict[str, Any]

per_key

per_key(metric: str = 'success') -> dict[str, float | None]

score_run

score_run(run_dir: Path, log_samples: list[dict[str, Any]], tasks: Mapping[str, Any], scorers: Sequence[Scorer]) -> list[dict[str, Any]]

vars_score

vars_score(score: Any) -> dict[str, Any]

evaluate

evaluate(benchmark: Benchmark, split: str, *, env_factory: Callable[[], Environment], agent: Agent | None = None, agent_factory: Callable[[], Agent] | None = None, run_root: str | Path, safety: Mapping[str, Any] | None = None, scorers: Sequence[Scorer] = DEFAULT_SCORERS, manifest: Mapping[str, Any] | None = None, run_id: str | None = None, record_privileged: bool = True, workers: int = 1, progress: Callable[[dict[str, Any]], None] | None = None, image_every: int = 1, timing: Timing | None = None) -> EvalResult

Run every TaskInstance of split. With workers > 1 each worker gets its own environment and agent (from the factories) and a disjoint shard of episodes; episode results do not depend on which worker ran them.

hylo.eval.scoring

Scorers read the trace only; they never touch the Agent or the Environment. A Score of None is an abstention (infra failure, agent error) and is excluded from metrics.

DEFAULT_SCORERS module-attribute

DEFAULT_SCORERS: tuple[Scorer, ...] = (SuccessScorer(), SafetyInterventionScorer(), StepsScorer())

SuccessScorer

name class-attribute instance-attribute

name = 'success'

score

score(trace: EpisodeTrace, task: TaskInstance) -> Score

SafetyInterventionScorer

name class-attribute instance-attribute

name = 'safety_interventions'

score

score(trace: EpisodeTrace, task: TaskInstance) -> Score

StepsScorer

name class-attribute instance-attribute

name = 'steps'

score

score(trace: EpisodeTrace, task: TaskInstance) -> Score

mean_ignoring_none

mean_ignoring_none(values: Iterable[float | None]) -> float | None

one_sided_exact_mcnemar

one_sided_exact_mcnemar(candidate_wins: int, parent_wins: int) -> float

P[X >= candidate_wins], X ~ Binomial(discordant, 0.5). Same test as Zetta gating.py.

paired_compare

paired_compare(candidate: Mapping[str, float | None], parent: Mapping[str, float | None]) -> PairedComparison

Pairs on TaskInstance.key (task id + seed). Pairs where either side abstained are dropped.

hylo.eval.stats

Gate tests (plugin group "gate_tests"): the paired statistic the improvement protocol uses to decide.

A gate test compares candidate and parent on the same (task, seed) keys and returns a PairedComparison: candidate_wins / parent_wins (pairs where one side is better, used by early rejection and futility), and a one-sided p_value for H1 "the candidate is better". The protocol around it (cascade, group-sequential looks, alpha spending, Holm, regression and safety vetoes, held-out report) is unchanged whichever test is chosen.

protocol: {gate_test: {type: sign, params: {metric: steps, direction: lower}}}

Built in: - mcnemar (default): exact one-sided McNemar on a binary metric (success); - sign: exact one-sided sign test on any metric, with a direction and an optional minimum difference; - paired_permutation: sign-flip permutation test on the mean paired difference of a continuous metric.

GateTest

Bases: Protocol

name instance-attribute

name: str

metric instance-attribute

metric: str

compare

compare(candidate: Mapping[str, float | None], parent: Mapping[str, float | None]) -> PairedComparison

McNemar

McNemar(metric: str = 'success')

name class-attribute instance-attribute

name = 'mcnemar'

metric instance-attribute

metric = metric

compare

compare(candidate, parent) -> PairedComparison

SignTest

SignTest(metric: str = 'success', direction: Literal['higher', 'lower'] = 'higher', min_diff: float = 0.0)

Pairs where the candidate is better by more than min_diff against pairs where it is worse.

name class-attribute instance-attribute

name = 'sign'

compare

compare(candidate, parent) -> PairedComparison

PairedPermutation

PairedPermutation(metric: str = 'success', direction: Literal['higher', 'lower'] = 'higher', n_resamples: int = 20000, seed: int = 0)

Sign-flip permutation test of the mean paired difference (exact when 2^n is small, Monte Carlo otherwise).

name class-attribute instance-attribute

name = 'paired_permutation'

compare

compare(candidate, parent) -> PairedComparison

binomial_check

binomial_check(test: GateTest, n: int = 40, trials: int = 200, seed: int = 0) -> dict[str, float]

Calibration helper for new tests: rejection rate at alpha 0.05 under H0 (identical agents) and under a clear effect. A valid one-sided test stays near or below 0.05 under H0.