python / searchhillclimb

Search

climber.search runs one search to the end, within a Budget, and leaves the result on the climber.

search is the whole search in one call, like fit on a model. It creates the run and the search, lets the climber work until the budget runs out, and returns the climber with the result on it. It is called search because that is what happens: the climber looks for a good solution within the budget, and a better one may well exist.

from hillclimb import Budget, Climber, Problem
from hillclimb.policies import Greedy
from hillclimb.selectors import Best

problem = Problem("heilbronn-11")
budget = Budget(wall_clock="10m", evaluations=40)   # the first limit reached ends the search
climber = Climber(selector_policy=Best(num_drafts=3), operator_policy=Greedy())

climber.search(
    problem,
    budget=budget,
    agent="claude-code",      # the coding agent that writes the solutions
    model="sonnet",
    learning=False,           # keep this search out of the folder's knowledge
)
climber.best                  # and so on, see Results
argumentwhat it does
problema Problem, or a problem id, a path, or a provider target like mlebench://…
budgeta Budget, see below. A plain "10m" or a number of seconds means Budget(wall_clock=...). Without it, the folder's run defaults (budget.total_s in runs/config.yaml); with none there, the call raises
agent, modelthe coding agent and its model. Otherwise the folder's defaults
holdoutscore the hidden split if the problem has one. On by default
learningread from and write to the folder's knowledge. On by default
seed_froma solution.py to score first, as the floor the search has to beat
namethe run's name
configa hillclimb.Config to use instead of the folder's hillclimb.yaml
logwhere the engine's log lines go. print by default, lambda *_: None for quiet
hintsprint where to follow the search live (hillclimb watch, tree, chart) when it starts. On by default; hints=False leaves the line out

Ctrl-C stops it. Pressing Ctrl-C (or interrupting a notebook cell) ends the search as stopped, prints how to resume it, and then raises KeyboardInterrupt, so a script that runs several searches stops there instead of starting the next one. hillclimb resume continues it.

You need a hillclimb dir for this: a folder with a hillclimb.yaml somewhere above the working directory. hillclimb init makes one. Anything you leave out comes from that folder's config. The run lands in runs/ like any other, and its spec.yaml records what you passed, budget included, so hillclimb run <run>/spec.yaml can run the same thing again.

The budget

A Budget says what a search may spend, in every dimension hillclimb counts. The first limit reached ends the search.

from hillclimb import Budget

Budget(wall_clock="10m")                                   # the clock alone
Budget(evaluations=30)                               # scored attempts, whatever the clock says
Budget(wall_clock="2h", evaluations=200, tokens=5_000_000, cost_usd=20)
dimensionwhat it counts
wall_clockwall-clock time, as "2h", "30m", "90s" or seconds. The search stops starting new work with a small margin left, so what is running can finish
evaluationsscored attempts. Each attempt counts one, each tune trial counts one, the baseline is free
tokensthe tokens the coding agent calls consumed, all kinds summed
cost_usdthe coding agents' bill, where the agent reports one. Reaching it parks the search instead of ending it, so it can be resumed with more

A dimension you leave out has no limit, except the wall clock, which falls back to the folder's run defaults in runs/config.yaml (with no clock anywhere, a budget in another dimension gets 2 hours; no budget at all is an error). The clock only counts while the search works. When you step a search by hand, the time you spend reading an outcome is yours.

search returns the climber, so you can chain it:

best = Climber(operator_policy=Greedy()).search(problem, budget=budget).best

A climber can search again, the same problem or another one. Each call is a new search, and the climber's attributes always refer to the latest.

The same thing as a function

from hillclimb import run, run_spec

outcome = run("heilbronn-11", climber=climber, budget=Budget(wall_clock="10m"))   # returns the SearchOutcome
outcomes = run_spec("run.yaml")                                           # every entry of a run spec, in order

run takes what search takes plus climber=, and returns the SearchOutcome instead of the climber. run_spec runs a whole run spec here, one entry after the other, where hillclimb run run.yaml would start a detached engine per entry.