Skip to content

The aggregator_factory exit — replacing the optimizer

Contract: factory(ledger, verifier, audit, config, staleness_policy) -> aggregator with ingest(card) and step() -> list[MergeReport] (and optionally finalize()), per AggregatorProtocol.

Every policy field swaps one decision inside the shipped seven-stage pipeline. Some optimizers need more: their own candidate pool, their own admission rule, per-instance score rows, parent switches. For those, aggregator_factory= replaces the optimizer wholesale — the sanctioned exit, and the one the mechanism-heavy ports have always used.

The single-head fact that makes this necessary

The engine's selection seam is honest about its limit: the ledger holds one live dev branch, so a selection policy that names any starting point other than the head is refused (_check_selection — "multi-head support is a separate change"). Population search on a single-head ledger therefore lives in the aggregator: keep the pool in the optimizer, and make "selection" a ledger commit that rewrites the head.

Implemented

Aggregator What it adds Where
Aggregator the shipped pipeline: dedupe, staleness, conflict, fusion, statistical acceptance, transactional commit, promotion agentdescent.aggregator
PopulationAggregator(Aggregator) an archive of every distinct committed head (with held-out score) + any standard SelectionPolicy picking the next parent, committed back to dev; finalize() lands the archive's best agentdescent.populationnot something you install yourself: Policies(selection=…) installs it, on both drivers (selection)
ParetoAggregator GEPA's pool with per-instance score rows and Algorithm-2 frontier sampling examples/gepa/
DGMArchiveAggregator DGM's keep-all archive with sigmoid(perf) × 1/(1+children) parent selection examples/dgm/
MetaSearchAggregator ADAS's keep-all archive over agent designs (head rule: shipped Beam(1)) examples/adas/
TopKFrontierAggregator EvoSkill's bounded top-K frontier (parent rule: FrontierBest), on every arm examples/evoskill/
StrictGateAggregator SkillOpt's strict gate (acceptance rule: StrictImprovement) + rejected-edit buffer + LR schedule examples/skillopt/
PromptBreederPopulation PromptBreeder's binary tournament: two units re-scored on a train batch, the loser's slot reused examples/promptbreeder/

The one trap

The factory path bypasses the default-aggregator construction, so Policies fields that the default path would wire (conflict, fusion, acceptance, promotion) do not reach a factory-built aggregator through the bundle. Pass them into your aggregator's constructor instead, and strip them from the bundle — carried in both places, one copy is silently ignored, which is the exact failure require_supported exists to prevent on the other path. agentdescent.population.population_factory shows the pattern.

A declared selection policy and an aggregator_factory= are refused together rather than resolved: both fill the aggregator seat, and a caller who passed both has nothing to read that says which one ran.

Prefer a policy field when one decision is enough; reach for the factory when your mechanism needs state the pipeline does not keep.