hylo-evolve¶
Hylo evolution layer: diagnosis, candidates, validation gate, registry
Repository hylo-harness, packages/hylo-evolve. Licence: Apache-2.0. Depends on: hylo-core, hylo-eval, hylo-learn.
Registered entry points
| Group | Name | Object |
|---|---|---|
| generators | critic.chunk_value |
hylo.evolve.critic:CriticProposer |
| generators | experience |
hylo.evolve.experience:ExperienceProposer |
| generators | learn |
hylo.evolve.learning:LearningProposer |
| generators | rule.gain |
hylo.evolve.loop:GainProposer |
| generators | rule.steering |
hylo.evolve.steering:SteeringRuleProposer |
| generators | selftrain.expert_iteration |
hylo.evolve.selftrain:ExpertIterationProposer |
| Module | Summary |
|---|---|
hylo.evolve |
Hylo evolution layer |
hylo.evolve.campaign |
Multi-cycle improvement campaigns: config, resume, budgets, manual approval (RFC-0002 §5) |
hylo.evolve.candidate |
Improvement candidates and the candidate registry |
hylo.evolve.conformance |
Conformance for generator plugins (proposers) |
hylo.evolve.critic |
Chunk-critic proposer (RFC-0002 L3): learn Q(s, chunk) from the campaign's own development traces and propose |
hylo.evolve.experience |
Turn improvement-loop outcomes into skills (RFC-0003 M3: the point where memory and RSI meet) |
hylo.evolve.learning |
Generic learning proposer (RFC-0004): train an artifact from this cycle's development runs and propose using it |
hylo.evolve.loop |
A single improvement cycle on standard splits: Execute(train) -> Diagnose -> Propose -> Check -> Validate -> |
hylo.evolve.protocol |
RSI evaluation protocol v2 (RFC-0002 §4): four-way split, cascade evaluation, archive, confirmatory gate |
hylo.evolve.selftrain |
Expert-iteration proposer (RFC-0002 L4): improve the policy's weights from its own successes |
hylo.evolve.steering |
Trace-based failure diagnosis and a rule-based proposer for frozen-policy steering (RFC-0002 L0/L1) |
hylo.evolve¶
Hylo evolution layer.
META
module-attribute
¶
META = {'contracts': {'generators': 'hylo.generator/v0.1'}, 'maturity': 'implemented', 'plugins': {'generators/critic.chunk_value': {'maturity': 'unverified', 'notes': 'not yet run in a campaign'}, 'generators/experience': {'maturity': 'unverified', 'notes': 'not yet run in a campaign'}, 'generators/learn': {'maturity': 'unverified', 'notes': 'not yet run in a campaign'}, 'generators/selftrain.expert_iteration': {'maturity': 'unverified', 'notes': 'smoke run only'}}}
hylo.evolve.campaign¶
Multi-cycle improvement campaigns: config, resume, budgets, manual approval (RFC-0002 §5).
A campaign repeats run_cycle: each promoted candidate becomes the next cycle's parent. State lives in the campaign
directory (registry, archive, campaign_state.json, per-cycle runs), so run_campaign resumes where it stopped.
With approval="manual", a candidate that passes the gate is parked in pending_approval.json and promoted only by an
explicit approve() (e.g. hylo evolve approve DIR): the default for anything that will run on hardware.
Every change of the active version is published as active.json (core.deployment), which is all a deploying
process (the policy server) reads.
EvolveConfig ¶
Bases: Contract
schema_version
class-attribute
instance-attribute
¶
experiment_overrides
class-attribute
instance-attribute
¶
proposer
class-attribute
instance-attribute
¶
diagnosis
class-attribute
instance-attribute
¶
protocol
class-attribute
instance-attribute
¶
stop_after_no_progress
class-attribute
instance-attribute
¶
load
classmethod
¶
A relative experiment path is resolved against the campaign file (else the working directory, as
configs written before 2026-11-01 assumed), so a campaign runs from any directory.
publish_active ¶
Write the campaign's active version (resolved agent spec) to <out_dir>/active.json.
run_campaign ¶
run_campaign(cfg: EvolveConfig, *, log: Callable[[str], None] = print, evaluate: Callable[[ExperimentConfig, str, str], Any] | None = None) -> CampaignState
shadow_config ¶
The active version, executing, with candidate_id riding along as a shadow that never acts (agents.shadow).
The step before trying a candidate on hardware: run this on the robot (or in simulation) and read agents.shadow.shadow_report to see how often and how far the candidate would have acted differently.
hylo.evolve.candidate¶
Improvement candidates and the candidate registry.
- A Candidate is declarative: a patch of dotted config paths (v0.1). Code candidates are not accepted until an isolated runtime exists (same stance as Zetta stages.py).
- ProtectedScope: candidates may not touch safety, benchmark, scoring or trace configuration.
- Registry: immutable versions with parent links, an
activepointer, an append-only ledger, and rollback as a first-class operation (no upstream project implements rollback).
PROTECTED_PREFIXES
module-attribute
¶
PROTECTED_PREFIXES = ('safety', 'benchmark', 'trace', 'scorers', 'evolution', 'schema_version', 'env')
CandidateRegistry ¶
Directory-backed registry: versions/
effective_patch ¶
Patches compose along the lineage, oldest first.
check_protected_scope ¶
check_protected_scope(candidate: Candidate, protected: tuple[str, ...] = PROTECTED_PREFIXES) -> None
hylo.evolve.conformance¶
Conformance for generator plugins (proposers).
check_generator ¶
hylo.evolve.critic¶
Chunk-critic proposer (RFC-0002 L3): learn Q(s, chunk) from the campaign's own development traces and propose steering the frozen VLA with it (sample N chunks, execute the best-valued one).
proposer: {type: critic.chunk_value, params: {data_roots: [runs/rsi-libero10/runs], n_samples: 4}}
The critic is trained in a subprocess (hylo.learn.critic) on search / search_val episodes only, so the gate still judges it on unseen init states. The offline same-state pair accuracy is recorded as evidence; a critic that cannot rank chunks from identical states better than chance (min_pair_accuracy) is not proposed.
CriticProposer ¶
CriticProposer(data_roots: list[str] | None = None, out_root: str = 'runs/critics', n_samples: int = 4, betas: tuple[float, ...] = (0.5,), image_camera: str | None = 'agentview', task_prefix: str | None = None, min_pair_accuracy: float = 0.55, agent_path: str = 'agent.params', python: str | None = None)
hylo.evolve.experience¶
Turn improvement-loop outcomes into skills (RFC-0003 M3: the point where memory and RSI meet).
Every archived candidate becomes a skill version whose status follows the evidence, never a model's opinion: - promoted -> verified (gate statistics as evidence) - gated_fail, effect <= 0 -> refuted - gated_fail, effect > 0 -> proposed (promising but underpowered: worth re-testing with more episodes) - rejected (stage-1 deficit) -> refuted - archived (not significant on search_val) -> proposed Refuted skills stay in the library so proposers can see what was tried and failed.
hylo evolve skills <campaign dir | archive.jsonl> --out runs/skills/libero10 --tags libero libero_10 --env libero
ExperienceProposer ¶
ExperienceProposer(inner: dict[str, Any] | Any, lessons: dict[str, Any] | Any | None = None, skills: dict[str, Any] | Any | None = None, reflector: dict[str, Any] | Any | None = None, skill_tags: list[str] | None = None, env: str | None = None, prefix: str = 'skill', top_lessons: int = 8, max_skills: int = 16)
Wraps any proposer with the experience layer (RFC-0003 M4). Before each proposal round:
1. the archive so far becomes skills (status from evidence);
2. lessons cited by decided candidates are credited (+1 verified / -1 refuted);
3. a reflector (optional) distils new lessons from the parent's search-split diagnosis;
4. the inner proposer sees diagnosis["lessons"] and diagnosis["skills"] (skill status only, no gate numbers).
proposer: {type: experience, params: {inner: {type: llm.steering, params: {...}},
lessons: {type: lesson_book, params: {path: runs/lessons/l10}},
skills: {type: skill_library, params: {path: runs/skills/l10}},
reflector: {type: llm.reflector, params: {...}}, skill_tags: [libero, libero_10], env: libero}}
classify ¶
(target status, evidence) for one archive entry.
skill_from_entry ¶
skill_from_entry(entry: ArchiveEntry, *, task_tags: tuple[str, ...], env: str | None, steering_path: str = STEERING_PATH, prefix: str = 'skill') -> Skill | None
skills_from_archive ¶
skills_from_entries ¶
skills_from_entries(entries: list[ArchiveEntry], library: Any, *, task_tags: tuple[str, ...] = (), env: str | None = None, prefix: str = 'skill') -> dict[str, Any]
Add one skill per new archive entry and move it to the status its evidence supports. Idempotent.
credit_lessons ¶
Outcome feedback: lessons a decided candidate cited get +1 (verified) or -1 (refuted). Once per candidate.
hylo.evolve.learning¶
Generic learning proposer (RFC-0004): train an artifact from this cycle's development runs and propose using it.
proposer: {type: learn, params: {
learner: critic.chunk_value, # "learners" plugin (runs in `python`'s venv)
params: {image_camera: agentview, task_prefix: libero/libero_10/},
python_path: agent.params.python, # or python: envs/libero/.venv/bin/python
require: {cv.same_state_pair_accuracy: 0.55}, # manifest metrics gate (offline sanity, not the RSI gate)
patch: {agent.params.ref: lerobot.critic_steered,
agent.params.params.critic: {path: "{artifact}", beta: 0.5}},
variants: [{agent.params.params.critic.beta: 0.0}],
hypothesis: "a critic ranking same-state chunks {metrics.cv.same_state_pair_accuracy} picks better chunks"}}
The learner seals its output (artifact.json: kind, role, digest, the runs it read); the proposer verifies it before proposing, and stage 0 of the gate verifies it again. Development splits only; a job whose artifact is already sealed with the same job digest is reused (campaign resume). CriticProposer and ExpertIterationProposer are the two original special cases of this pattern.
LearningProposer ¶
LearningProposer(learner: str, patch: dict[str, Any], params: dict[str, Any] | None = None, variants: list[dict[str, Any]] | None = None, data_roots: list[str] | None = None, splits: tuple[str, ...] = DEV_SPLITS, out_root: str = 'runs/learned', python: str | None = None, python_path: str | None = None, env_path: str | None = None, require: dict[str, float] | None = None, hypothesis: str = '', unit: str = 'L3', timeout_s: float | None = None, online: bool = False)
propose ¶
propose(diagnosis: Mapping[str, Any], cfg: ExperimentConfig, parent_id: str | None, archive: list[ArchiveEntry]) -> list[tuple[Candidate, str]]
hylo.evolve.loop¶
A single improvement cycle on standard splits: Execute(train) -> Diagnose -> Propose -> Check -> Validate ->
Held-out -> Promote. For benchmarks with train / validation / heldout splits and quick demonstrations; campaigns
(the cascade and sequential gate of RFC-0002) use protocol.run_cycle. Both take the same generators (the
Proposer contract, hylo.core.improvement).
Split discipline: diagnosis reads the train split (privileged state allowed); candidate
selection uses the validation split (paired, same seeds); heldout is only reported.
FailureReport ¶
GateResult ¶
CycleReport ¶
GainProposer ¶
GainProposer(path: str = 'agent.params.kp', factors: tuple[float, ...] = (2.0, 4.0), adaptive: bool = False)
Scale one numeric config path. adaptive=True reads the diagnosis' failure buckets: mostly timeouts scale up, mostly oscillation or safety aborts scale down (by the inverse factors).
diagnose ¶
Deterministic failure bucketing from the trace (an LLM diagnoser would add hypotheses on top).
gate ¶
gate(candidate_val: EvalResult, parent_val: EvalResult, *, alpha: float = 0.05, max_safety_increase: float = 0.0, max_task_regression: float = 0.1) -> GateResult
improvement_cycle ¶
improvement_cycle(base_cfg: ExperimentConfig, registry: CandidateRegistry, proposer: Proposer, evaluate: Evaluate, *, approve: Callable[[Candidate, GateResult], bool] = lambda c, g: True, alpha: float = 0.05, gate_kwargs: Mapping[str, Any] | None = None) -> CycleReport
hylo.evolve.protocol¶
RSI evaluation protocol v2 (RFC-0002 §4): four-way split, cascade evaluation, archive, confirmatory gate.
search split -> diagnosis and proposer feedback (the proposer may read these traces)
search_val split -> candidate selection (metrics never shown to the proposer)
gate split -> one confirmatory paired test per selected candidate (Holm if several)
heldout split -> report only
Lessons applied (our research notes): no single-shot acceptance; selection and confirmation on different seeds; held-out sealed; novelty check before spending rollouts; every candidate kept in an archive with its hypothesis and evidence; promotion only through the gate; budget accounting.
Archive ¶
spend ¶
Nominal level for look k (1-based) under the power-family spending function; sums to alpha over all looks.
gate_sequential ¶
gate_sequential(finalists: list[ArchiveEntry], parent_cfg: ExperimentConfig, evaluate: Evaluate, p: ProtocolConfig) -> dict[str, tuple[list[StageResult], bool]]
Evaluate look by look; at look k test the cumulative pairs with Holm at the spent level; stop early on success or futility. Overall type-I error <= alpha by the union bound over looks.
run_cycle ¶
run_cycle(base_cfg: ExperimentConfig, registry: CandidateRegistry, archive: Archive, proposer: Proposer, evaluate: Evaluate, diagnose: Diagnose, *, protocol: ProtocolConfig | None = None, approve: Callable[[Candidate, dict[str, Any]], bool] = lambda c, g: True) -> CycleReport
hylo.evolve.selftrain¶
Expert-iteration proposer (RFC-0002 L4): improve the policy's weights from its own successes.
Each call collects the parent's episodes on the search split with full camera recording, fine-tunes the policy on the successful ones in the policy's own venv (hylo.policies.lerobot.selftrain), and proposes the new weights as a config patch. The candidate then goes through the same cascade and sequential gate as any steering change, so a fine-tune that only memorised the search states is rejected on unseen init states.
proposer: {type: selftrain.expert_iteration, params: {steps: 300, batch_size: 8, lr: 1.0e-5}}
Only development splits are trained on; the proposer reuses the parent's search traces if they already have
per-step images, otherwise it records a fresh collection run (trace.image_every = 1).
ExpertIterationProposer ¶
ExpertIterationProposer(out_root: str = 'runs/selftrain', steps: int = 300, batch_size: int = 8, lr: float = 1e-05, full: bool = False, collect_split: str = 'search', min_successes: int = 20, weights_path: str = 'agent.params.params.repo_id', python_path: str = 'agent.params.python', env_path: str = 'agent.params.env', timeout_s: float = 6 * 3600, learner: str = 'lerobot.expert_iteration')
hylo.evolve.steering¶
Trace-based failure diagnosis and a rule-based proposer for frozen-policy steering (RFC-0002 L0/L1).
Diagnosis reads only the search split's traces (it may use privileged state; the gate and held-out splits never
reach the proposer). The proposer turns failure buckets into steering patches, each with a falsifiable hypothesis.
An LLM proposer implements the same Proposer protocol (hylo.evolve.protocol).
SteeringRuleProposer ¶
Maps failure buckets to steering patches. Each candidate is one coherent change with a hypothesis.
episode_features ¶
episode_features(trace, eef_key: str = 'eef_pos', gripper_key: str = 'gripper_qpos', still_eps: float = 0.0015, closed_below: float = 0.06) -> dict[str, float]
diagnose_traces ¶
diagnose_traces(result: EvalResult, stall_steps: int = 30, regrasp: int = 3, **kw: Any) -> dict[str, Any]
Failure buckets: stall (a still period >= stall_steps), regrasp (>= regrasp grasp attempts), other.