Evolve your harness
Harness evolution improves an agent by changing its text rather than its weights. The text is a tree of configuration, rules, prompts, skills, and extension code, and reef publishes an improved version of that tree the same way it publishes weights. No GPU is involved anywhere in this path.
The harness_evolve recipe supplies the versioning and evaluation loop. You
supply what to change and how to decide whether the change is better. Its YAML
configuration follows the same structure as the recipes in
define a recipe.
The contract#
You supply up to three callables, named in the recipe config as dotted
module:attribute references.
propose(nodes, samples) -> Mutation | Sequence[Mutation] | None is the
method. It receives the current composition as (kind, config) pairs and the
batch of TraceSample records, each a recorded request with its reported
score. It returns one Mutation (create, update, or remove on one
entry), a sequence of them as a single composite proposal that applies under
one snapshot and settles under one verdict, or None to skip the step.
evaluate(task, episode_result) -> float grades one finished episode of one
task. The mechanism runs both the candidate and the current composition on
every configured task and hands each result here.
acceptance(candidate_scores, current_scores) -> bool is optional and decides
publication from the two per-task score vectors in task order, where None
marks an episode that could not run. Two policies ship: pairwise, the
default, publishes when the candidate wins more task pairings than it loses,
and always publishes every applied mutation while still recording the
scores.
The mechanism owns everything else, and a method should not rebuild any of it. That covers node validation at proposal time, rendering the tree through the adapter descriptor, running the paired episodes, writing each step's metrics to the commit log, publishing a winning tree as a versioned artifact, and reverting the tree when the policy says no.
Beyond the callables, a recipe on this kind supplies tasks, the prompts every
step is scored against, and seed, the nodes a first boot starts from. Keep
tasks short, since it sets the cost of every step.
The tree#
A composition is a flat list of entries. Each entry has an id, a node kind, and
a config, and ids are root level, so a mutation naming a nested id is rejected.
Five node kinds are registered in
reef/harness/nodes.py:
| kind | what it contributes |
|---|---|
config | a JSON object deep-merged into one of the adapter's declared config targets |
rules | text appended to the agent's rules file |
agent_command | a named prompt template |
skill | a named SKILL.md |
code_extension | a named code file the harness loads in process |
The tree renders into a directory of text files through an adapter descriptor,
and that directory is the artifact. Two adapters ship, pi and opencode,
each declaring its own config targets and paths, so a tree written for one does
not necessarily render under the other.
One step#
A step begins when the processor has batched enough scored reports. reef
snapshots the tree, calls propose, and applies what comes back. It then
renders the candidate tree and runs every task twice, once against the
candidate and once against the current tree, so a step costs 2 × len(tasks)
headless episodes.
An episode that could not run scores nothing and ranks below every real score, so the side that failed loses that task. Both sides failing is a tie. The acceptance policy then decides, and anything short of publication restores the snapshot.
Every step writes its tally to the commit log whether it published or not, so a run's history is readable without rerunning anything.
The worked instance#
examples/skillclaw_repro rebuilds
SkillClaw on this contract. Its propose is the paper's night: it digests one
day of recorded traffic, judges the sessions, groups them by referenced skill,
takes one decision per group, and returns the pool diff as one composite
mutation sequence, so many skill edits settle under one verdict. Its evaluate
grades three exact-answer probe tasks. Its acceptance policy is always,
because the paper's night publishes every non-skip decision and lets the next
day measure it. The batch is the day, batch_size: 60 with no score window.
examples/harness_evolve is the
smaller starting point, a minimal method on the same mechanism.
Mapping another method#
GEPA, reflective prompt evolution, maps onto the same three callables.
Reflective mutation is propose: read the failing traces from the batch, build
a reflection prompt from them, and have the model rewrite one rules node or
one agent_command node. The candidate pool is the version chain, since every
accepted mutation publishes a version whose parent is the version it beat, so
there is no reason to build a parallel population. Pareto selection is an
acceptance policy over the per-task score vectors:
def pareto_acceptance(
candidate: tuple[float | None, ...],
current: tuple[float | None, ...],
) -> bool:
"""Accept when no task regresses and at least one improves."""
improved = False
for cand, curr in zip(candidate, current, strict=True):
cand = cand if cand is not None else float("-inf")
curr = curr if curr is not None else float("-inf")
if cand < curr:
return False
improved = improved or cand > curr
return improved
Name it as acceptance: my_pkg.gepa:pareto_acceptance and the mechanism does
the rest. ACE is the same shape again, a skill node proposer on this mechanism
(#176).
Reading the result back#
A published tree is an artifact version like any other. The harness pulls it
over GET /reef/harness and pins what it installed, which
connect your harness covers alongside the rest of
the wire contract.
What not to write#
Do not write a new backend class. A method that seems to need one is asking for
a propose, an evaluate, or an acceptance it does not have yet.
Do not keep a parallel ledger. The commit log already records every step's mutation, scores, and verdict, and a second record store will disagree with it after the first crash.
Do not bypass the acceptance seam. Publishing outside the step, by writing
artifacts directly or mutating the tree without a verdict, breaks revert and
makes the version chain lie. The ungated mode is acceptance: always, not a
bypass.
Where this stands#
This path is younger than the weights path, and two gaps are worth knowing before planning work against it.
reef ships no proposer and no evaluator in the package itself, so there is no
bundled method here the way sao and tttd are bundled methods on the weights
path. Proposer and Evaluator are abstract base classes, and the worked
methods live under examples/.
The paired episodes currently run batched by side rather than interleaved per task, which weakens the comparison the pairing exists to make.