climbers / tuner-and-memoryhillclimb

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:

tunerwhat it does
randomrandom values within the declared ranges
optunaOptuna'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).

On this page