diff --git a/agentdescent/evolvespec.py b/agentdescent/evolvespec.py index 3ea9fe3f..22778265 100644 --- a/agentdescent/evolvespec.py +++ b/agentdescent/evolvespec.py @@ -1179,10 +1179,8 @@ def run(skill: str, task: Task) -> str: run=run, propose=reflector(reflect), strategy=SeededAppendRules(seed=_read_text_target(spec.target)), blast_radius=SKILL_BLAST_RADIUS) - notes.append( - "kind=text: SeededAppendRules keeps the seed prompt and appends each " - "accepted one-sentence rule under '# Learned rules' (#837); pass " - "strategy=SingleSlot(...) in evolve knobs for full-rewrite semantics") + # The SeededAppendRules note is added after knobs merge, once we know + # whether a card or caller replaced strategy (#879). # Inline text is supported, but a path-looking missing file is almost # certainly a typo -- warn so plan surfaces it (#706). if looks_like_path_target(spec.target) and not os.path.isfile( @@ -1348,6 +1346,14 @@ def run(skill: str, task: Task) -> str: # -- knobs: kind defaults < spec.evolve < caller overrides ------------------ knobs: Dict[str, Any] = {**defaults, **(ov.knobs if ov else {}), **_evolve_pass_through(spec.evolve), **overrides} + # kind=text defaults to SeededAppendRules above; a card (gepa/ace/dgm/ + # skillopt) or compose(strategy=...) puts "strategy" in knobs and then + # overwrites that default. Don't also claim SeededAppendRules (#879). + if spec.kind == "text" and "strategy" not in knobs: + notes.append( + "kind=text: SeededAppendRules keeps the seed prompt and appends each " + "accepted one-sentence rule under '# Learned rules' (#837); pass " + "strategy=SingleSlot(...) in evolve knobs for full-rewrite semantics") # The dedicated field and the pass-through agree; an explicit `spec.evolve` # entry wins, exactly as it would for any other knob. if spec.extra_artifacts: diff --git a/changelog.d/879-text-strategy-note.md b/changelog.d/879-text-strategy-note.md new file mode 100644 index 00000000..b717a526 --- /dev/null +++ b/changelog.d/879-text-strategy-note.md @@ -0,0 +1,4 @@ +Fixed +When a `kind=text` card or caller sets `strategy`, plan no longer also +claims `SeededAppendRules`. `gepa` / `ace` / `dgm` / `skillopt` drop the +generic note; `rrsi` and a bare text spec still show it (#879). diff --git a/tests/test_seeded_append_text.py b/tests/test_seeded_append_text.py index 6e1cdf85..fbca9838 100644 --- a/tests/test_seeded_append_text.py +++ b/tests/test_seeded_append_text.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from agentdescent.evolution import SeededAppendRules, SingleSlot, Task, evolve from agentdescent.evolvespec import EvolveSpec, compose from agentdescent.strategies import rule_id @@ -81,3 +83,43 @@ def test_empty_state_preserves_seed_trailing_newline(): """apply no-op depends on empty render matching the on-disk seed (#849).""" seed = "original\n" assert SeededAppendRules(seed=seed).render({}) == seed + + +_SEEDED_NOTE_PREFIX = "kind=text: SeededAppendRules" + + +def _text_spec(**extra): + return EvolveSpec( + kind="text", + target=SEED, + data={"inline": [{"prompt": f"q{i}", "gold": "a"} for i in range(8)]}, + agent="echo", + reflect="echo", + **extra, + ) + + +def _has_default_seeded_note(notes): + return any(n.startswith(_SEEDED_NOTE_PREFIX) for n in notes) + + +@pytest.mark.parametrize("algorithm, expect_note", [ + (None, True), + ("rrsi", True), + ("ace", False), + ("dgm", False), + ("gepa", False), + ("skillopt", False), +]) +def test_text_seeded_append_note_follows_actual_strategy(algorithm, expect_note): + """Cards that replace strategy must not also claim SeededAppendRules (#879).""" + extra = {} if algorithm is None else {"algorithm": algorithm} + comp = compose(_text_spec(**extra)) + assert _has_default_seeded_note(comp.notes) is expect_note + + +def test_text_seeded_append_note_drops_when_caller_sets_strategy(): + """A compose(strategy=...) override is the same as a card knob (#879).""" + comp = compose(_text_spec(), strategy=SingleSlot(initial_value=SEED)) + assert isinstance(comp.kwargs["strategy"], SingleSlot) + assert not _has_default_seeded_note(comp.notes)