Skip to content

Verifier–curriculum co-evolution

A verifier is a proxy. A curriculum that only samples tasks the verifier already scores well measures the proxy against itself, so the two can agree while both drift from the oracle. This is the Goodhart failure mode of a self-improving loop, and it is not fixed by auditing the verifier on a random sample: the tasks where the proxy is wrong are the ones a random sample is least likely to pick.

VerifierCurriculum makes the task distribution target that disagreement:

from agentdescent import VerifierCurriculum

cur = VerifierCurriculum(pool, verifier=verifier_score, oracle=oracle_score)
rows = cur.probe(artifact)                 # (task, verifier, oracle, gap) per task
hard = cur.select(artifact, n=8)           # largest |verifier - oracle| first
report = cur.coevolve(artifact, rounds=3, n=8, update=fix_verifier)
print(report.initial_abs_gap, report.final_abs_gap, report.improved)

select(mode=...) chooses the direction: abs (default) targets any disagreement, overestimate targets a verifier that is too generous, and underestimate one that is too harsh. coevolve probes the pool once per round, ranks those rows, and feeds each selected task's already-paid oracle label to update(task, oracle_score); the caller mutates its verifier (a correction table, a fitted judge, a prompt), and the next round's verifier score is re-measured without buying the oracle again. The oracle cost is therefore one label per pool task per round -- a full-truth probe, stated honestly: this finds disagreement by paying for truth, it does not infer it from the verifier alone. The report is the gap before and after, so "did the curriculum help the verifier?" is a measurement rather than a hope.

At the acceptance seam

VerifierAgreement is an AcceptancePolicy that audits a fixed probe set on a random fraction of commits and refuses a gain the verifier claims and the oracle denies:

Policies(acceptance=VerifierAgreement(
    audit_tasks, verifier=verifier_score, oracle=oracle_score,
    inner=default_acceptance, sample_rate=0.2))

It wraps the inner policy (it can only make acceptance stricter), and an inner rejection is never overridden. inner=None is the shipped DefaultAcceptance, configured through the forwarded configure hook. An oracle regression on the audit set is refused whatever the verifier says; a verifier-only gain is oracle-disagrees. The audit draw is seeded from the diff id, so it does not depend on merge order.

Both pieces take (artifact, task) -> float callables, so they work with the shipped verifier, a model judge, or a deterministic fixture. They are deliberately separate from meta_audit: that store records sampled truth for a run, while this module selects the tasks on which truth is most informative and lets the verifier learn from them.