-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.
run() and script the user’s turns — see the Agent docs.)
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]).
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. 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_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.env.taskset.task.judge.model in your config (it is a string).