Strategies — what evolves, and how a proposal becomes a diff¶
Modules: agentdescent.strategies
(text) · agentdescent.treestrategy
(a directory) — one module per strategy family, none of them inside the engine
· API: Strategy, SingleSlot, AppendRules, KeyedRules, FileTree
A strategy answers two questions and nothing else:
- What is the artifact? — its state, as a flat
{key: value}dict. - What does a proposal mean? — how a string from the reflector becomes a
Diff.
class Strategy(Protocol):
def initial(self) -> Dict[str, str]: ...
def render(self, state: Dict[str, str]) -> str: ...
def to_diff(self, state, proposal, author, base_version, target) -> Optional[Diff]: ...
Three methods, no base class. Everything else — merging, conflict resolution, acceptance, versioning — is the aggregator's job and needs no cooperation from you.
The four built-ins¶
from agentdescent import SingleSlot, AppendRules, KeyedRules, FileTree
evolve(tasks, reward, agent=agent, strategy=SingleSlot(initial_value="Answer concisely."))
| strategy | the artifact is | competing proposals |
|---|---|---|
SingleSlot |
one value — a system prompt, an instruction, one document | always contradict; the best replaces the incumbent |
AppendRules |
a deduped list of lessons, keyed by content hash | almost always fuse; identical ones collapse |
KeyedRules(categories) |
one entry per named category | contradict within a category, fuse across |
FileTree(files) |
a directory, one key per file path | contradict per file, fuse across files |
The key space is the design decision¶
The keys decide what can merge concurrently, and that is the whole choice:
AppendRules keys = hash(proposal) → N workers, N distinct lessons, all fuse
SingleSlot keys = {"value"} → N workers, N candidates, one survives
KeyedRules keys = your categories → parallelism bounded by category count
FileTree keys = file paths → parallelism bounded by file count
SingleSlot maximises selection pressure: every round is a tournament and only
the best-scoring rewrite survives. AppendRules maximises accumulation: almost
everything merges, and the artifact grows. Neither is better; they answer
different questions about what you are evolving.
Split the artifact to buy parallelism
Two complementary edits to the same key are a contradiction, and one of them
is thrown away. If your workers keep colliding, the fix is usually to give
the artifact more keys — categories for KeyedRules, or more files for
FileTree (a small SKILL.md plus references/*.md, one concern per file).
SingleSlot¶
SingleSlot(initial_value="You are a helpful assistant.",
key="value", min_chars=1, empty_render="(no instruction yet)")
The most common thing anyone evolves, and until it existed three of the shipped
algorithm ports each rolled their own variant.
min_chars guards against a reflector that replies with a terse non-answer.
AppendRules¶
Each proposal becomes a rule keyed by rule_id(text) — a content hash — so two
workers that independently learn the same lesson produce the same key with the
same value, which the aggregator collapses to one. Rules are rendered sorted
under the title.
KeyedRules¶
Proposals look like "formatting: always answer in cents". A proposal for an
existing category overwrites it, so two workers disagreeing about formatting
produce a contradiction the aggregator resolves on held-out score. An
unrecognised category falls back to content-addressed append behaviour.
keys() declares the category list, which is what
tensor parallelism partitions into sections.
FileTree¶
The artifact is a directory; a key is a relative file path. It has its own page: evolving a directory.
Writing your own¶
from agentdescent import Diff
class OneValue: # this is SingleSlot, longhand
def initial(self):
return {}
def render(self, state):
return state.get("v", "(none)")
def to_diff(self, state, proposal, author, base_version, target):
if state.get("v") == proposal:
return None # None -> propose nothing
return Diff(diff_id=f"{author}:{base_version}", target=target,
ops={"v": proposal}, author=author)
evolve(tasks, reward, agent=agent, strategy=OneValue())
Return None from to_diff for any proposal you do not want — malformed, a
no-op, out of bounds. That is the normal path, not an error path: a reflector
that ignores your protocol is a quality problem the run should absorb and count,
not crash on.
Four things to get right:
rendermust be deterministic, because it is the evaluation-cache key. Two states that render identically cannot score differently, and two states that render differently must be genuinely different — see below.- Keys are your op-space. Pick them so that things which can be improved independently land on different keys.
to_diffsees the currentstate. Use it to drop no-ops and to enforce whatever bounds you want before the aggregator's trust region does.- Declare
keys()if you know the key space up front. Without it,evolve()refuses to pair the strategy withTensorParallelrather than silently dropping most proposals.
render is the artifact's serialisation, not necessarily its prompt¶
render(state) feeds two things: run(rendered, task), and the evaluation cache
key (EvolvingArtifact._signature).
For a text artifact those wants coincide. For a structured one they do not — a
lossy pretty-printed render would make two different artifacts share a cached
score. The resolution is that run is where an artifact becomes a prompt:
def run(rendered, task):
state = parse(rendered) # back to structure
return model(my_prompt_format(state, task))
That is exactly how FileTree keeps a lossless JSON
render while EvoSkill still shows the model its own
### skill: <name> format — byte for byte what it showed before its artifact
became a directory.
The framework never injects the artifact into your prompt — you do
run(rendered, task) hands you the artifact as text. Where it goes is
entirely your call: a system prompt, a prefix, a few-shot block, a tool
description, or a file on disk. So "what evolves" is set by two things
together — strategy= fixes the artifact's shape, run= decides how that
shape reaches the model.
Strategies in the algorithm ports¶
Each is a real Strategy you can read and reuse:
| strategy | port | the artifact |
|---|---|---|
ACEPlaybook |
ACE | an itemised, incremental-delta context playbook |
InstructionSlot |
GEPA | one instruction prompt each proposal replaces |
SkillLibraryTree |
EvoSkill | a directory of SKILL.md files (a FileTree subclass) |
SkillDocStrategy |
SkillOpt | one markdown doc under bounded edit operations |
AgentDesignStrategy |
ADAS | one agentic-system design |
HarnessStrategy |
DGM | a coding agent's capability set |