Write your own scaffold¶
A scaffold is a choice of six components plus an optional bit of adaptation. There are two ways to
build one: compose components inline (no new files), or subclass GalapagosScaffold and ship
a card so others can load it by name.
Way 1 — build-your-own from components¶
Pass component instances (or import paths) straight to GalapagosScaffold.from_card. Any role you
omit gets a sensible default; the Evaluator always comes from the task.
import galapagos as gx
from galapagos.components import (
InMemoryPopulation, ExploreExploitPolicy, DefaultPromptBuilder, DiffProposer, NullMemory,
)
scaffold = gx.GalapagosScaffold.from_card(
population=InMemoryPopulation(capacity=200),
selection_policy=ExploreExploitPolicy(seed=0, explore_ratio=0.4, num_inspirations=3),
prompt_builder=DefaultPromptBuilder(),
proposer=DiffProposer(),
memory=NullMemory(),
model=gx.load_model("openai/gpt-4o-mini", host="openrouter"),
)
result = scaffold.run(task=gx.load_task("playground_sphere"), max_iterations=30)
print(result.best_score)
Each of population, selection_policy, prompt_builder, proposer, evaluator, memory may be:
- a component instance (as above),
- a dotted
module.Classpath (e.g."galapagos.components.UCBBanditPolicy"), or - a
.pyfile path containing exactly one subclass of the right base class.
This is the fastest way to prototype: swap one slot (say, UCBBanditPolicy for
ExploreExploitPolicy) and re-run. See the six components for every
shipped implementation.
Way 2 — subclass + register + ship a card¶
For a reusable method, subclass GalapagosScaffold, override build_components, and register it.
This is exactly how the bundled OpenEvolveScaffold and AdaEvolveScaffold are built.
The class¶
# my_scaffold/scaffold.py
from galapagos.scaffolds import GalapagosScaffold, register_scaffold
from galapagos.components import (
IslandPopulation, UCBBanditPolicy, DefaultPromptBuilder, DiffProposer, ScratchpadMemory,
)
from galapagos.config import GalapagosConfig
from galapagos.models import GalapagosModel
from galapagos.records import Genome
@register_scaffold("banditevolve")
class BanditEvolveScaffold(GalapagosScaffold):
name = "banditevolve"
@classmethod
def build_components(cls, config: GalapagosConfig, model: GalapagosModel | None) -> dict:
"""Return the five scaffold-side components. The Evaluator comes from the task."""
seed = int(config.get("seed", 0))
n = config.get("database.num_islands", 4)
return {
"population": IslandPopulation(num_islands=n, migration_interval=20),
"selection_policy": UCBBanditPolicy(seed=seed, num_islands=n,
c=config.get("selector.ucb_c", 1.4)),
"prompt_builder": DefaultPromptBuilder(include_memory=True),
"proposer": DiffProposer(),
"memory": ScratchpadMemory(max_notes=config.get("memory.max_notes", 8)),
}
# ---- optional adaptation hooks (no-ops by default) ----
def before_step(self) -> None:
"""Runs before selection each step — adjust intensity, ε, mode, ..."""
def after_step(self, child: Genome, result) -> None:
"""Runs after a child is evaluated (result is None on a no-op step).
Here: periodically distill the best feedback into the meta-scratchpad."""
interval = self.config.get("memory.write_interval", 10)
if result is None or self.state.iteration % interval != 0:
return
best = self.state.best
feedback = best.artifacts.get("text_feedback") if best else None
if feedback:
self.memory.write(f"iter {self.state.iteration}: best={best.fitness:.4f} — {feedback}")
def periodic(self) -> None:
"""Runs once per iteration, after the step — strategy refresh, migration triggers, ..."""
@register_scaffold("banditevolve") wires the class to its card name and adds it to
registered_scaffolds(). Importing the module is enough to register it.
The hooks¶
The base loop is fixed; subclasses adapt through three no-op hooks called each step:
| Hook | When | Typical use |
|---|---|---|
before_step() |
before selection | mode switching, intensity / ε schedules |
after_step(child, result) |
after the child is evaluated (result is None on a no-op) |
bandit credit, stagnation response, memory writes |
periodic() |
once per iteration, after the step | meta-scratchpad refresh, strategy co-evolution, migration |
You also have the full run state on self: self.state (a RunState: iteration, cost_usd,
best, signals), self.population, self.memory, self.config, self.model, and self._stale
(iterations since the last best-score improvement).
The card¶
Ship a card.yaml with your package so the method is loadable by card; the controller field
points at your class. (Bundled scaffolds keep theirs next to the code, at
src/galapagos/scaffolds/<name>/card.yaml; for your own package any path works — you load it
with from_card(path=...).)
# my_scaffold/card.yaml
name: banditevolve
display_name: BanditEvolve
type: test_time_search
tier: search
status: stable
summary: "UCB-routed island evolution with a meta-scratchpad."
description: |
A bandit SelectionPolicy routes the budget toward the most rewarding islands; a meta-scratchpad
Memory accumulates distilled design insights.
source: "your repo / paper"
license: Apache-2.0
controller: my_scaffold.scaffold.BanditEvolveScaffold
components:
population: {kind: island}
selection_policy: {kind: ucb_bandit}
prompt_builder: {kind: default_with_memory}
proposer: {kind: diff}
evaluator: {kind: task}
memory: {kind: scratchpad}
model:
default: openai/gpt-5.5
host: openrouter
roles: [propose]
requirements: {gpu: none, docker: optional, python: ">=3.10"}
defaults_config: config.yaml
The config¶
Ship a config.yaml with the defaults your build_components reads. It lives next to the code
(the bundled scaffolds keep theirs at src/galapagos/scaffolds/<name>/config.yaml); when bundled it
loads via GalapagosConfig.from_config(scaffold_name="banditevolve"), otherwise via path=:
# my_scaffold/config.yaml
seed: 0
budget:
max_iterations: 100
patience: null
database:
num_islands: 4
migration_interval: 20
selector:
ucb_c: 1.4
memory:
max_notes: 8
write_interval: 10
Run it¶
import my_scaffold.scaffold # the @register_scaffold decorator fires on import
import galapagos as gx
scaffold = gx.GalapagosScaffold.from_card(
"banditevolve",
config=gx.GalapagosConfig.from_config(path="my_scaffold/config.yaml"),
model=gx.load_model("openai/gpt-4o-mini", host="openrouter"),
)
result = scaffold.run(task=gx.load_task("playground_sphere"), max_iterations=40)
print(result.summary["scaffold"]) # -> "banditevolve"
gx.available_scaffolds() and galapagos scaffold list show the cards bundled in the galapagos
package (under src/galapagos/scaffolds/); a third-party scaffold loads by explicit path
(from_card(path="my_scaffold/card.yaml")) or, once its module is imported (or registered via the
galapagos.scaffolds entry-point group), by its @register_scaffold name. To submit it to the Hub,
see Submit to the Hub.