-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.
-U,--add-user— also scaffold avf.Usersimulator atservers/user.py- Use this to simulate a user interacting with the model. Not all harnesses support user simulation.
-H,--add-harness— also scaffold a customvf.Harnessatharness.py, selectable via--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.
--taskset.num-tasks and --taskset.task.tolerance, or with the equivalent TOML fields.
Lazy and infinite tasksets
load() may be a generator instead of returning a list: yield each task as it’s built.
Consumers materialize tasks through Taskset.select, which pulls only what a run needs —
eval -n 5 builds 5 tasks, not the whole set — 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:
num_tasks (-n on the CLI — omitting it is an error), and shuffle is a no-op (warned):
there is no whole set to sample from, and the first n generated tasks are already an
arbitrary sample. Generation must be deterministic — env-server pool workers each run
their own load() and rely on every worker producing the same sequence, so seed any
randomness with a constant (see alphabet_sort_v1, color_codeword_v1, 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 init MY_ENV -T):
Using Judges
If your reward is semantic, use an LLM judge.taskset.task.judge.model in your config (it is a string).