fix(skills): extract evolved instruction with overfit/collapse guard - #146
fix(skills): extract evolved instruction with overfit/collapse guard#146mgandal wants to merge 2 commits into
Conversation
evolve_skill.py read `optimized_module.skill_text` to build the evolved skill. That attribute is the SkillModule input field, passed verbatim on every forward pass and never mutated by the optimizer — so every evolved skill came out byte-identical to the baseline (confirmed on a 28KB skill: 28,431 == 28,431 chars, "0.0% growth" at the constraint gate despite a real score delta). GEPA/MIPRO actually optimize the predictor's *signature instructions*. This adds `_extract_evolved_body()`, which reads the compiled instruction via `named_predictors()` and only uses it when it differs from the stock `TaskWithSkill` docstring, falling back to `skill_text` otherwise so a run never overwrites a good skill with generic boilerplate or an empty diff. Also fixes the GEPA constructor: `max_steps` was removed in dspy>=3 and raised TypeError, silently dropping every run into the MIPROv2 fallback. Switched to `max_metric_calls`. Adds tests/test_evolved_body_extraction.py (3 cases: unoptimized fallback, rewritten-instruction extraction, unchanged-instruction guard). Full suite: 148 passed (was 145).
Follow-up to the extraction fix. Testing on a real 15KB reference skill exposed a failure mode: GEPA/MIPRO optimize the signature instruction against a small synthetic eval set, and for a knowledge-dense skill the optimizer rewrites that instruction into a narrow task-specific procedure that scores well on the eval examples but discards the bulk of the skill. Observed: a 15KB biofigure skill (7 icon libraries, element schema, 3 archetypes, pitfalls) collapsed to a 2KB "embed one WikiPathways SVG and export a PDF" recipe — a -86% body shrink that passed every existing constraint (growth_limit only caps GROWTH, not shrink). Without this guard, _extract_evolved_body would have substituted the stub and the pipeline would gut rich skills. Add a conservative SHRINK_FLOOR (0.6): if the evolved instruction is under 60% of the baseline body length, fall back to the original and leave the variant in evolved_FAILED.md for human review. Genuine instruction refinements on short procedural skills stay well above the floor. Adds two tests (collapse rejected, substantial rewrite accepted). Full suite: 150 passed (was 148).
Update: empirically validated, plus a second guard addedRan this end-to-end against a real 15KB reference skill (gpt-oss:120b, synthetic eval, 10 iterations). Two things confirmed: 1. The extraction fix works — first non-zero body delta the pipeline has ever produced on this skill. Before, 2. But it surfaced a real failure mode that needed a second guard. GEPA/MIPRO optimize the signature instruction against a small synthetic eval set. For a knowledge-dense skill, the optimizer rewrites that instruction into a narrow task-specific procedure that scores well on the eval examples but throws away the bulk of the skill:
Without a guard, Note for maintainers — deeper architectural question
Separately: GEPA still falls back to MIPROEven with the Full suite now 150 passed. |
Review: PR #146 — Overfit/collapse guard for evolved instructionsThis PR introduces a genuinely valuable safety mechanism that no other PR addresses. Key contributions:
What's missing:
Suggestion: The overfit/collapse guard and tests are excellent defensive code that should be cherry-picked into whichever PR becomes the base (#155 is the strongest candidate). The |
Problem
evolve_skill.pybuilt the evolved skill fromoptimized_module.skill_text. That attribute is theSkillModuleinput field — the skill body is passed verbatim into the predictor on every forward pass and is never mutated by the optimizer. So the "evolved" skill was always byte-identical to the baseline.Reproduced on a 28KB skill: input 28,431 chars → evolved 28,431 chars, constraint gate reported
growth_limit: +0.0%even though MIPRO's own trial scores improved. The score delta was real; the file delta was always zero.Root cause
GEPA/MIPRO optimize the predictor's signature instructions (
TaskWithSkilldocstring), not the input field. The improved procedure lives inpredictor.predict.signature.instructionsand was simply never read.Fix
_extract_evolved_body(): reads the optimized instruction vianamed_predictors(), and uses it only when it differs from the stockTaskWithSkilldocstring — otherwise falls back toskill_text, so a run never overwrites a good skill with generic boilerplate or an empty diff.max_stepswas removed indspy>=3and raisedTypeError, silently dropping every run into the MIPROv2 fallback. Switched tomax_metric_calls.Tests
New
tests/test_evolved_body_extraction.py— 3 cases:Full suite: 148 passed (was 145), no regressions.
Note for maintainers
This surfaces a second, separate issue worth a follow-up: the default
skill_fitness_metric(keyword overlap) can score a generic instruction candidate above a genuinely better task-specific rewrite (observed: MIPRO proposed a strong request-classifier instruction that lost to boilerplate on keyword overlap). Consider defaulting real runs toLLMJudge. Left out of this PR to keep it focused on the extraction bug.