-p,--path <dir>— parent directory, default:./environments-T,--add-tool— also scaffold avf.Toolsettool server atservers/tool.py- Use this to create custom tools which are installed into supported harnesses via MCP.
-H,--add-harness— also scaffold a customvf.Harnessatharness.py, selectable via--env.agent.harness.id <name>- Prefer a built-in harness unless the model needs to run inside a custom program.
For a production-scale catalog of tasksets, see the companion research-environments repository.
An example taskset
Tasksets are made of the following components:- The Taskset loads the actual Tasks from a dataset using the
load()function. It can be configured with the TasksetConfig, to e.g. load a certain split. Configs are exposed to the user and thus should only contain configurable values. - A Task defines the scoring, stop conditions, setup, judging etc. of the task to solve. It also gets the tools or user config. It gets configured by a TaskConfig, e.g., to set a specific judge model.
- The TaskData is the immutable object that holds the actual data, i.e., the prompts, images, expected outputs etc., as well as other information such as timeouts (if set).
vf.TaskConfig, so no task values (like judge models) are configurable.
The scaffold also exports the taskset from addition_v1/__init__.py:
AdditionTaskset is what verifiers loads and makes discoverable for evaluation.
Data and configuration
Keep values on the narrowest object that needs them:- Put load-time values shared across the dataset, such as its split, name, seed, or size, on
TasksetConfig. - Put values used by every task during execution or scoring under
TasksetConfig.task.
--env.taskset.num-tasks and --env.taskset.task.tolerance, or with the equivalent TOML fields ([env.taskset]).
Task identity
A task’sidx is its index in the load() stream; it is set automatically. Set TaskData.id to a durable ID from the source (e.g. a dataset’s instance ID) and TaskData.name to a readable name; both are optional.
A task’s hash identifies its exact serialized data. Its key provides stable identity across runs and defaults to the hash. Override Task.key with a durable source ID when task data contains run-local fields such as idx; keys must be unique within a taskset. Both values are recorded on traces.
Selecting tasks
Views on a taskset pick which tasks an iteration yields. They are lazy and chain in any order:include keeps and exclude drops the tasks named by idx (ints and Python slices start:stop:step, e.g. "100:", ":50", "::2"; no negative positions), ids, keys or names; a task matches when any list names it. An entry that matches no task logs a warning once the stream was read.
The eval, debug, validate and GEPA entrypoints take a select block and apply it with taskset.select(config.select), which chains the views in a fixed order: include, exclude, shuffle, skip, limit:
skip and limit, a larger limit extends the same selection instead of drawing a new one.
Lazy and infinite tasksets
load() may be a generator instead of returning a list: yield each task as it’s built. Consumers iterate the taskset lazily (eval -n 5 builds 5 tasks, not the whole set, unless shuffle needs all of them) — so a generator pays off whenever building a task is expensive.
A procedural taskset can keep yielding forever. Declare INFINITE = True so consumers know the stream never ends — infinity is inherent to the taskset, not a config knob; how many tasks a run takes is the run’s choice (-n), not the taskset’s:
take (-n on the CLI) or an include of only closed idx ranges — omitting both is an error — and shuffle needs a bound before it: there is no whole set to sample from otherwise. select applies limit after the shuffle, so on the CLI only closed include.idx ranges can bound it (--select.include.idx 0:1000 -s -n 50); in Python, take(n).shuffle() works too. The generator runs once, client-side (the eval entrypoint or the prime-rl orchestrator pulls tasks off it and ships each task’s data to the env server), so nothing needs to re-produce the same sequence across processes; keep load() deterministic only if you want --resume to regenerate the same first n tasks (see alphabet_sort, color_codeword, or the built-in textarena taskset).
Adding Tools
Some tasksets require custom tools, which are bundled as avf.Toolset (similar to how a vf.Taskset bundles vf.Task). Tools are exposed as MCP servers to the given harness and thus need a harness which exposes MCP support (via SUPPORTS_MCP).
You can create them like this (remember the bootstrapping with uv run vf-init MY_ENV -T):
Using Judges
If your reward is semantic, use an LLM judge.env.taskset.task.judge.model in your config (it is a string).
Plugging hooks via config
Stop conditions, metrics, and rewards can also be plugged into any task from config, without touching the taskset’s code. Each entry names an async function by import path (pkg.module.function, pkg.module:function, or path/to/file.py:function); like decorated hooks, functions declare what they need by parameter name (task, trace, runtime — stops receive the trace only):
@vf.stop / @vf.metric / @vf.reward methods; a plugged hook replaces a decorated one with the same name, so an existing signal can be swapped out (like single_turn above). Rewards take a weight, and every entry takes a priority (higher runs first); unset fields keep the replaced method’s values.
Leaving fn out keeps the task’s own decorated method and only overrides its metadata — for example, turning an existing zero-weight reward into a training signal: