Create or migrate native verifiers.v1 taskset, environment, and harness packages. Use to build a taskset, port a benchmark, add task tools, script or model a user, build a multi-agent environment, package an agent harness, or migrate an existing v0 environment to the typed v1 trace model.
72
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Create native v1 tasksets that are installable and runnable with verifiers.
To start, ALWAYS use the CLI to create a package with the correct files:
uv run init my-task-v1Add only the components the contract needs:
uv run init my-task-v1 -T # task toolset
uv run init my-agent-v1 -H # custom reusable harnessOften, the user does not want nor need a custom reusable harness, as verifiers offer a lot of built-in ones.
For some common tasks, there are existing, pre-built tasksets in the verifiers.v1.tasksets folder. These come with batteries included and should always be preferred. The most notable inclusion is the HarborTaskset, which allows the creation of Harbor-based tasksets within a few LoC (also see docs/v1/harbor.md).
When a task needs a custom container image (e.g. a Harbor task whose task.toml does not have docker_image), you can build and publish it with prime images push from the Prime CLI (Documentation). This builds in the cloud — no local Docker needed — and prints the full image reference to use as the task's image field.
Use the naming convention <env>.x86.<task>:latest for the image name (e.g. abc.x86.xyz:latest), where <env> is the taskset name and <task> is the individual task.
Before starting with the implementation, think about the following things:
run()), not a server.Env subclass — or an existing bundled env (--env.id best-of-n|agentic-judge|user-sim) already covers it.For a port, map source behavior one-to-one: rows, the exact prompts verbatim, harness restrictions, score extraction and exceptions.
Ask the user about unresolved semantic choices instead of inventing them. Present your evidence (both in code and in your questions) by commenting and linking to the exact source in the paper, the GitHub repo etc.
A package exports one vf.Taskset subclass — and optionally one vf.Env subclass (multi-agent control flow) and/or one vf.Harness subclass — through __all__. The taskset export happens automatically when you bootstrap a new taskset using uv run init.
Do not add load_environment(), load_taskset(), or load_harness() functions. The v1 loader resolves classes and their config types from __all__ and generic bases.
Use:
import verifiers.v1 as vfNever mix v0 Environment, Rubric, Parser, SingleTurnEnv, MultiTurnEnv, or ToolEnv objects into a v1 taskset. Exclusively use functions, classes and objects from verifiers.v1.
import verifiers.v1 as vf
# One row's serializable data. Add references or other task-specific fields here.
class AdditionData(vf.TaskData):
answer: int
# The behavior for that row. Decorated methods may request only the values they need;
# `trace` contains the full message graph and `self.data` is this task's row.
class AdditionTask(vf.Task[AdditionData]):
@vf.reward
async def exact_match(self, trace: vf.Trace) -> float:
return float(trace.last_reply == str(self.data.answer))
# The taskset is the loader. Its config can be the empty base config.
class AdditionTaskset(vf.Taskset[AdditionTask, vf.TasksetConfig]):
def load(self) -> list[AdditionTask]:
# Construct one behavior object around each data row and the shared task config.
return [
AdditionTask(
AdditionData(idx=i, prompt=f"What is {i} + {i}?", answer=2 * i),
self.config.task,
)
for i in range(100)
]
# Export the taskset class so the v1 loader can discover it.
__all__ = ["AdditionTaskset"]Do not override Taskset.__init__. Implement load() on the taskset and put hooks and scoring on the task.
TaskData owns the immutable, serializable values for one row:
Only TaskData is stored on the trace. Do not put live clients, runtime handles etc. here.
Task owns the behavior applied to that row:
setup, finalize, and model-free validate hooks;self.config.Taskset owns loading and selection-time concerns. Its load() constructs the tasks, its direct config fields hold dataset/split/seed/sample-count knobs, and Taskset.toolsets may construct task-agnostic servers shared by one environment worker's rollouts.
The harness owns:
Runtime config chooses where code executes. Task hooks should use the vf.Runtime interface they receive instead of assuming Docker-, Prime-, Modal-, or host-specific implementation details.
Env.finalize(task, episode) — attach via trace.record_reward/record_metric, in program order; no live runtime there.TaskError.Implement Task.validate(self, runtime) whenever ground truth can be checked without a model. Keep rollout work on the task:
setup(self, trace, runtime) — prepare files or services.finalize(self, trace, runtime) — capture artifacts needed for scoring.@vf.reward / @vf.metric — evaluate while the runtime is still live.Persist inspectable artifacts in JSON-serializable trace.info. Put counters and live coordination in a typed vf.State subclass.
Some tasksets require custom tools. These should be the exception as they don’t work with every harness and are registered as MCP servers.
class SearchToolset(vf.Toolset[vf.ToolsetConfig]):
TOOL_PREFIX = "search"
@vf.tool
async def query(self, text: str) -> list[str]:
# Tool docstrings are exposed to the model as the MCP tool description.
"""Search the task corpus."""
return []
class SearchTaskConfig(vf.TaskConfig):
tools: vf.ToolsetConfig = vf.ToolsetConfig()
class SearchTask(vf.Task[vf.TaskData, vf.State, SearchTaskConfig]):
# Constructing on Task.toolsets gives it one-server-per-rollout scope.
@classmethod
def toolsets(cls, config: SearchTaskConfig) -> list[vf.Toolset]:
return [SearchToolset(config.tools)]
if __name__ == "__main__":
SearchToolset.run()Choose placement from the tool's lifetime and filesystem needs:
Task.toolsets with a vf.ToolsetConfig field. One server is launched per rollout. The default subprocess runtime is inexpensive and host-side.colocated = true on its ToolsetConfig when the tool must see the harness's filesystem or processes. It still launches once per rollout.vf.SharedToolsetConfig, put its config field directly on TasksetConfig, and construct the server in Taskset.toolsets.url on the toolset's config. Verifiers connects to the streamable-HTTP MCP endpoint instead of launching the class locally.There is one mechanism: the interaction — agents.<name>.interaction(task) in the env's run(); whoever calls turn() is the run's user, one harness segment per turn (the program yields, the caller answers, the next segment resumes the exchange with the answer). A prompt-less task is opened by the first turn(message); a prompted task speaks first (bare turn()); to hide a scenario prompt from the wire, hand the interaction a task copy with prompt=None and keep scoring on non-prompt fields (the user-sim shape). There is no user server to declare or place; who computes the turns is env control flow:
Env.run() override — see environments/alphabet_sort or the bundled textarena taskset.agents.user.interaction(...) and relayed into the assistant's run — or just use the bundled user-sim env (--env.id user-sim), which does exactly this from the task's prompt-as-scenario.The harness running the assistant must be able to resume an exchange: transcript-backed resume (SUPPORTS_RESUME) covers the default relaunch-on-the-conversation (bash, null), and a harness with its own session state overrides resume() natively (codex).
When a desired interaction pattern is more than one agent run, export an Environment subclass along with the taskset: declare each agent as a vf.AgentConfig field with a default instance on a vf.EnvConfig subclass (bound via Environment[YourConfig], read as self.config, addressed as --env.<agent>.*) — the field name is the agent's name, the only naming site, and per-run caps (turns, tokens, stage timeouts, retries) are agent fields. A declared pin is its author default; an unpinned agent runs the taskset's default harness, and its model context defaults to the run's own. Task x agent fit validates per run, on the task each agent actually receives (an env-minted task carries its own tools/NEEDS_CONTAINER, so a bare verdict task pairs with any taskset). Then write run(task, agents) (imperative control flow, returning nothing — every finished run joins the episode automatically, stamped with its standing), and optionally setup(agents) (env-hardcoded standing, e.g. agents.judge.trainable = False) and finalize(task, episode) (sibling-dependent judgement over episode.traces, via record_reward/record_metric; trace.agent_name names each agent). Before writing one, check the bundled envs (--env.id best-of-n | agentic-judge) and the reference implementation (environments/code_golf). See docs/v1/env.md.
Choose a built-in first (which you can find under verifiers.v1.harnesses). Add a custom harness only when desired or not currently implemented.
Its launch() must point every model request at the provided endpoint with secret (to work with the InterceptionServer); direct provider calls bypass trace capture and thus would break downstream usage.
Advertise capabilities accurately:
SUPPORTS_MCPSUPPORTS_RESUMEAPPENDS_SYSTEM_PROMPTReturn the vf.ProgramResult from runtime.run_program() or runtime.run_uv_script(). Do not manually build trace nodes in the harness.
If the harness creates per-rollout state outside the runtime's disposable workspace,
remove it in an idempotent cleanup(trace, runtime) override. Cleanup runs after
scoring and before the runtime is released, including for borrowed runtimes.
pyproject.toml.pyproject.toml or uv.lock for a taskset dependency.Map concepts directly:
| V0 | Native v1 |
|---|---|
| Dataset row | Typed vf.TaskData subclass |
load_environment(**kwargs) | Exported vf.Taskset class + typed config |
Rubric reward function | Task @vf.reward method |
| Parser object | Ordinary parsing inside task scoring |
ToolEnv tools | vf.Toolset constructed in Task.toolsets or Taskset.toolsets |
MultiTurnEnv.env_response | an interaction loop in the env's run() |
| Dict state | Typed vf.State |
| Sandbox subclass | Runtime config + task hooks |
Preserve prompt, tool, and scoring equivalence before improving design. Compare representative v0 and v1 traces where feasible.
After package installability, validation, and representative eval behavior are stable, ask the user whether Hub visibility should be PUBLIC or PRIVATE. Only then run:
prime env push my-task-v1 --visibility PRIVATEPublishing is an external state change and requires the user's requested visibility. Do not publish merely because local verification passed.
c51c094
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.