Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agentdescent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
AuditedReward, AuditRecord, AuditStore, DeferredOracle, GoldAnswer,
NullOracle, Purpose, RenderTap, resolve_from_mapping, verifier_fingerprint,
residual_bias,
BoostState, VerifierBoost, isotonic_fit,
)
from . import backends, dataloader # submodules: agentdescent.dataloader.hf_rows(...)
from .dataloader import Dataset, split_dataset
Expand Down
1 change: 1 addition & 0 deletions agentdescent/audit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"""

from .wiring import Audit, attach
from .boost import BoostState, VerifierBoost, isotonic_fit
from .calibrator import (STALE_INFLATION, Calibrator, Rectification,
population_resid_sd)
from .coverage import (CALIBRATION_CEILING, CALIBRATION_FLOOR, MIN_UNSEEN,
Expand Down
351 changes: 351 additions & 0 deletions agentdescent/audit/boost.py

Large diffs are not rendered by default.

33 changes: 32 additions & 1 deletion agentdescent/audit/tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ def _take_signature() -> str:
return signature



def _resolve_version_extra(extra: Any) -> Any:
"""Call a live ``version_extra`` (e.g. ``boost.version_tag``) before fingerprinting.

``verifier_fingerprint`` stringifies ``extra`` via ``repr``. A bound method's
``repr`` does not change with the boost's state, so a callable must be
evaluated first or activation would be invisible to the audit.
"""
if callable(extra):
return extra()
return extra


class AuditedReward:
"""A cheap verifier that hands a sampled minority of its work to the truth.

Expand Down Expand Up @@ -185,8 +198,12 @@ def __init__(self, verifier: Callable[[Any, str], float], *,
self.rates = dict(rates or {})
self.calibration_fraction = calibration_fraction
self.seed = seed
# Keep the ingredients so a live ``version_extra`` (e.g. a boost's
# ``version_tag``) can be re-fingerprinted when the boost activates.
self._version_extra = version_extra
self._explicit_verifier_version = verifier_version
self.verifier_version = verifier_version or verifier_fingerprint(
verifier, extra=version_extra)
verifier, extra=_resolve_version_extra(version_extra))
#: Units seen, and units audited. A sample rate that never fires is the
#: quiet failure this makes loud.
self._warn_about_the_rates()
Expand All @@ -197,6 +214,20 @@ def __init__(self, verifier: Callable[[Any, str], float], *,
self.skipped = 0
self._lock = threading.Lock()

def refresh_verifier_version(self) -> str:
"""Re-derive ``verifier_version`` from a live ``version_extra``.

Used when an attached :class:`~agentdescent.audit.boost.VerifierBoost`
activates: its ``version_tag`` changes and the audit's fingerprint
(gate, watch, store filter) must follow, or the old calibration keeps
applying to a judge that has moved.
"""
if self._explicit_verifier_version is not None:
return self.verifier_version
self.verifier_version = verifier_fingerprint(
self.verifier, extra=_resolve_version_extra(self._version_extra))
return self.verifier_version

# -- the Reward contract -------------------------------------------------

def __call__(self, task: Any, output: str) -> float:
Expand Down
36 changes: 33 additions & 3 deletions agentdescent/audit/wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ class Audit:
#: :meth:`~agentdescent.audit.gate.VerifierWatch.check` between rounds, or
#: :meth:`~agentdescent.audit.gate.VerifierWatch.on_merge` from a hook.
watch: VerifierWatch
#: Optional verifier bootstrap flywheel (see
#: :class:`~agentdescent.audit.boost.VerifierBoost`). ``None`` keeps the
#: old behaviour exactly. When set, :meth:`recompute` retrains it after
#: re-estimating the rectification, so the loop's calibration and the
#: verifier's own improvement move together.
boost: Optional["VerifierBoost"] = None

@property
def verifier_version(self) -> str:
Expand All @@ -85,7 +91,23 @@ def rectification(self) -> Rectification:
return self.calibrator.current(self.verifier_version)

def recompute(self) -> Rectification:
"""Re-read the store and re-estimate. Call after a batch resolves."""
"""Re-read the store and re-estimate. Call after a batch resolves.

When a :class:`~agentdescent.audit.boost.VerifierBoost` is attached,
it is retrained here too -- a batch of resolved labels is exactly the
moment the flywheel should learn from. After a retrain that changes
``version_tag``, the reward's fingerprint is refreshed and
:meth:`~agentdescent.audit.gate.VerifierWatch.check` runs so the old
calibration is withdrawn before the new version is estimated.
"""
if self.boost is not None:
# Train against labels scored under the version currently in force
# (before activation changes the tag).
self.boost.retrain(verifier_version=self.verifier_version)
prev = self.verifier_version
self.reward.refresh_verifier_version()
if self.reward.verifier_version != prev:
self.watch.check()
return self.calibrator.recompute(self.verifier_version)

def rebalance(self, unseen: float, **kw: Any) -> float:
Expand Down Expand Up @@ -173,7 +195,8 @@ def attach(verifier: Callable[[Any, str], float], *,
verifier_version: Optional[str] = None,
version_extra: Any = None,
watch_ids: Iterable[str] = (),
watch_globs: Sequence[str] = ()) -> Audit:
watch_globs: Sequence[str] = (),
boost: Optional["VerifierBoost"] = None) -> Audit:
"""Assemble the audit around ``verifier`` and return what ``evolve()`` needs.

Parameters
Expand Down Expand Up @@ -228,12 +251,19 @@ def attach(verifier: Callable[[Any, str], float], *,
if not policy.enabled:
enabled = False

if boost is not None and version_extra is None and verifier_version is None:
# Live tag so activation changes the audit fingerprint (review #273).
version_extra = boost.version_tag

reward = AuditedReward(
verifier, oracle=oracle, store=store, draw_by=draw_by,
sample_rate=default_rate, stratify=stratify, rates=rates,
calibration_fraction=calibration_fraction, seed=seed,
verifier_version=verifier_version, version_extra=version_extra)

if boost is not None and boost._version_for_training is None:
boost._version_for_training = lambda: reward.verifier_version

calibrator = Calibrator(store, alpha=policy.alpha if policy else 0.05,
seed=seed)
acceptance = RectifiedAcceptance(
Expand All @@ -251,4 +281,4 @@ def attach(verifier: Callable[[Any, str], float], *,
watch.check()
return Audit(reward=reward, run=RenderTap(run) if run is not None else None,
acceptance=acceptance, store=store, calibrator=calibrator,
watch=watch)
watch=watch, boost=boost)
14 changes: 14 additions & 0 deletions changelog.d/273-verifier-boost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Added
Verifier bootstrap flywheel (`agentdescent/audit/boost.py`, #273):
`VerifierBoost` wraps any verifier and learns a monotone calibration curve
(`isotonic_fit`, numpy-only PAV) from the audit's **IMPROVEMENT** pool, then
activates it **only if it provably reduces residual error on a held-out slice
of that pool** (the bootstrap gate). When active it corrects the cheap and
learned layers the aggregator uses to rank candidates and the audit uses for
its trust signal; `eval_counts` / `full_eval` are delegated **raw**, so the
commit gate keeps the oracle's authority over what commits. It trains on the
IMPROVEMENT pool only (never the CALIBRATION pool that measures it), filters
to the current verifier version, and declares itself as a new verifier version
(`version_tag`) so `attach(boost=...)` / `Audit.recompute()` refresh the audit
fingerprint and `VerifierWatch` withdraws the old calibration. Default
behaviour unchanged -- no boost means no fitting and no correction.
8 changes: 6 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,7 @@ AuditedReward(
|---|---|
| `calibration_set() -> list` | Resolved CALIBRATION records for *this* verifier version. |
| `pending() -> list` | Audited units still waiting on truth. |
| `refresh_verifier_version() -> str` | Re-derive `verifier_version` from a live `version_extra`. |

### `RenderTap(run: Callable[[str, Any], str]) -> None`

Expand All @@ -1316,7 +1317,8 @@ Audit(
acceptance: RectifiedAcceptance,
store: AuditStore,
calibrator: Calibrator,
watch: VerifierWatch
watch: VerifierWatch,
boost: Optional['VerifierBoost'] = None
) -> None
```

Expand Down Expand Up @@ -1350,7 +1352,8 @@ attach(
verifier_version: Optional[str] = None,
version_extra: Any = None,
watch_ids: Iterable[str] = (),
watch_globs: Sequence[str] = ()
watch_globs: Sequence[str] = (),
boost: Optional['VerifierBoost'] = None
) -> Audit
```

Expand All @@ -1373,6 +1376,7 @@ attach(
| `version_extra` | `Any` | `None` | |
| `watch_ids` | `Iterable[str]` | `()` | Artifact ids and diff-key globs that mean the verifier changed. Nothing is watched by default, which is right for a fixed function and exactly wrong for a run that evolves its own judge -- see `VerifierWatch`. |
| `watch_globs` | `Sequence[str]` | `()` | As `watch_ids`. |
| `boost` | `Optional['VerifierBoost']` | `None` | |

---

Expand Down
Loading
Loading