5. Tuner and memory
Parameter trials on a candidate, and what a search learns from others.
Tuner
A solution may declare numeric knobs in a params.json next to solution.py (restart counts,
step sizes, temperatures). The operator policy then runs extra trials of that same code with other
values, and keeps the best. Which values each trial tries is the tuner's choice:
| tuner | what it does |
|---|---|
random | random values within the declared ranges |
optuna | Optuna's sampler over the trials so far. pip install 'hillclimb[optuna]' |
climber: {tuner: optuna, tuner_params: {seed: 7}}A tune trial needs no coding agent, so it is cheap. The values that won ship with the solution
in best/params.json, and a candidate improved from a tuned one starts from them. Seeds are never
tuned: the spread across seeds is the noise, not a knob. The problem side of this is in
Optional features.
Memory
Memory is what a search reads from earlier searches before it starts, and what it leaves for later
ones. The built-in files memory lives in your hillclimb dir under knowledge/ and can be
versioned with git:
- every finished search writes a card (what worked, what failed) that later searches on the same family of problems get as a prior-experience section in their prompts,
- searches running at the same time share live cards,
- a cheap agent pass turns a finished search into typed claims (this helps, that does not), which feed the knowledge graph.
climber: {memory: files, memory_params: {max_cards: 1, claims: false}}memory: none keeps nothing. Whether learning is on at all is yours, not the climber's:
learning.enabled in runs/config.yaml, or --no-learning on a run.
A memory of your own overrides any of five steps, each of which does nothing by default:
# notebook.py
from pathlib import Path
from hillclimb.sdk import Memory, Retrieved
class Notebook(Memory):
DEFAULTS = {"where": "notes.txt"}
def retrieve(self, *, context=None): # before the first attempt
notes = Path(self.param("where"))
return Retrieved(text=notes.read_text() if notes.exists() else "")
def record(self, journal, *, budget_s, cost_usd): # after the search
best = max((c.val_score for c in journal.scored_candidates()), default=None)
Path(self.param("where")).write_text(f"Last time the best was {best}.\n")The other three are bind(env) (when the search starts), live() (on every prompt) and
publish(...) (after every result).