@env.template,
the shape that prompts the agent and scores what happens. Calling that template mints a Task: a
plain data row (an env name, a template id, bound args) that travels over the wire and runs anywhere.
The agent loop and grading happen in the gap between the generator’s two yields, and a call to
run() executes one row and returns a Job.
Defining a task
A task is an async generator with exactly two yields. The first yield is the prompt; the generator pauses while the agent works; the agent’s answer comes back into the generator; the second yield is the reward (0.0-1.0).
tasks.py
float, an EvaluationResult,
or a dict with a numeric score; the env layer normalizes any of them to the reward.
Template
@env.template() registers the generator as a template: a callable that mints a concrete Task
without running anything.
Declare
returns=T and the answer arrives as a parsed Answer[T]
(.content parsed, .raw the original string); without it, answer is the raw string the agent
submitted.
One template spans a space of tasks. Call it with different arguments and a single function becomes a
whole dataset, with no separate artifact per task:
tasks.py
Task
ATask is a Pydantic model - one portable, validated row of data. It holds no live environment: env
is a name, the join key between the row and whatever brings that environment up at run time. So a task
runs anywhere without an env object in-process - the prompt and reward arrive over the wire from the
substrate that placement brings up.
When you don’t have the template in hand (data pipelines, generated rows), build the model directly -
the model is the row, so
task.model_dump() and Task.model_validate(data) are the whole codec:
Grading
The second yield is the reward. Return a float inline, or reach for the comparison helpers, async graders, and composed grades documented in graders. For how to design a reward that separates good work from bad and resists hacking, see designing tasks.Tasksets
ATaskset is a named collection of task rows. Build one in code, or load it from a source:
ts.to_file("tasks.json") (or .jsonl). Tasksets are also ordered
collections:
Running
taskset.run(agent, ...) executes every task and returns a Job.
task.run(...) is the same call over a taskset of one, with identical semantics.
A crashed rollout comes back as a failed
Run inside the job rather than
raising, so one bad rollout never collapses a batch. For the end-to-end workflow see
running an eval.
Syncing
Sync publishes a locally-authored taskset to hud.ai so you can run it there, compare models on it, and browse its traces; local runs never need it. Thehud sync tasks <name> command
uploads a taskset and only what changed, a workflow covered in
creating an environment. In code,
diff(local, remote) returns a SyncPlan describing the comparison:
Once a reward separates good work from bad, designing tasks covers the
difficulty and spread that make a taskset worth training on, and graders the
scoring that produces each reward.