nemo_gym.data.tasks.harbor.parameterized
nemo_gym.data.tasks.harbor.parameterized
Parameterized tasks: one template folder plus tasks.jsonl, rendered into plain task folders at prepare.
A template folder is a task folder (task.toml, environment/, tests/ or verifier.py, …) with a
tasks.jsonl beside it, one row per task: {"task_id": "...", "task_data": {...}} (or the fields flat
beside task_id). Files named <stem>.template<suffix> (instruction.template.md,
solution/solve.template.sh, solution/answer.template.md) are rendered with the row’s fields,
{{ task_data.question }} or {{ question }}, into <stem><suffix>; every other task file is copied,
hidden files under environment/, tests/, solution/ and steps/ included (environment/.env,
environment/.dockerignore: the task digest and the sandbox uploads see them, so the render must carry them).
Left out: the editor leftovers and byte-code caches the digest skips (is_ignored)
at any depth, and at the template root the hidden entries (.git, .venv), tasks.jsonl, the dataset-level
files and prepare’s records, plus README.md, which describes the template, not a task (a per-task README is
README.template.md). A rendered name that collides with another file of its folder (instruction.md beside
instruction.template.md) is refused, naming both.
Each rendered folder gets the row’s fields as task_data.json, which the loader reads and the dataset’s
task_data.py validates (nemo_gym.data.tasks.harbor.task_data). The rendered task.toml is the template’s
with [task].name rewritten to <org>/<task_id> (the template’s org): Harbor keys a dataset on that name
(Task.name, harbor add merges folders that share one, trial results and the viewer label by it), so every
rendered task needs its own; a template without a [task] table renders without one (Harbor then names the task
after its folder). Dataset-level files (dataset.toml,
task_data.py, verifier.py) are copied beside the rendered folders, which then form an ordinary
dataset the loader, the environment server and the publisher read as files.
Rendering is deterministic, so rendered folders digest the same on every prepare of the same template. The
rendered folder is also the dataset root prepare records into, so a re-render replaces the task folders and the
dataset-level files but keeps prepare’s own records there (manifest.toml, compose-images.json,
built-images.json and their lock files): an image built for the template is reused, not rebuilt and pushed
again, and a base image’s recorded configuration is not re-resolved on every run. The render lands in a hidden
sibling of the output first and is then swapped in entry by entry: a task folder whose rendered content did not
change is left in place, a changed one is replaced with a rename, a stale one removed. An identical re-render
therefore touches no task folder at all, so a server still seeding episodes from an earlier prepare of the same
template (it copies each task folder at seed) never meets a folder that is being deleted and rewritten. The swap
runs under an exclusive lock on a hidden sibling of the output (.<name>.lock, the record files’ flock), since
gym dataset validate and gym eval run render one template into one folder and may do so at once: the second
install waits for the first and then finds its folders already in place, rather than racing it entry by entry. A
record writer’s in-flight temporary at the root (a hidden *.tmp) is kept like the records themselves, so a prepare
recording into the folder while another renders it never loses the file it is about to install.
Module Contents
Classes
Functions
Data
API
Bases: HarborTaskError
tasks.jsonl or a template file cannot be rendered.
Swap the entries of fresh into output: stale entries go (records stay), unchanged task folders and files
are left in place, changed ones are replaced by rename.
Rename fresh to target, whatever is there now: a file over a file atomically, otherwise the old entry is
renamed aside first and removed after the new one landed, so the gap is one rename wide, not a tree’s rewrite.
Refuse a task_data.json (or a task_data.template.json) at the template root.
The rendered task’s task_data.json is written from its row once the tree is rendered, so a file the template
ships under that name would be copied or rendered and then replaced without a word, the author’s fields discarded.
Below the root the name is task content (a tests/ fixture) and is copied like any other file.
Render source into target: templates filled, every other entry copied with its mode.
What the digest skips (__pycache__, *.pyc, editor leftovers) is left out at every depth; hidden entries
only at the template root (.git, .venv, an editor’s folder), since below it a hidden file is task content
(environment/.env, environment/.dockerignore) that the digest covers and the sandbox receives. The root
.gitignore is the exception: Harbor’s packager reads it as the task’s ignore rules (harbor task init writes
one into every task), so every rendered task carries the template’s, and publish ships it on. Two entries
that would land under one name (a plain instruction.md beside instruction.template.md) are refused, and so
is a root entry that would land under task_data.json (_refuse_task_data_twins, checked before any row).
Whether two rendered trees hold the same entries, byte for byte and mode for mode (no links in either).
Whether name is an org/name Harbor accepts for a task or a dataset (its ORG_NAME_PATTERN, no ..).
A record prepare writes at the dataset root, its lock file, or a record writer’s in-flight temporary: kept when the rendered folder is replaced.
The temporaries are the hidden *.tmp names the writers use (.<record>.<pid>.tmp from
write_json_record, .manifest-<pid>-<thread>-<hex>.tmp from the
manifest’s writer); the renderer writes no such name itself. A writer in another process fsync\ s and then
os.replace\ s its temporary over the record, and loses the file to an overlapping render otherwise.
A task folder with tasks.jsonl beside its task.toml.
The rows of <template>/tasks.jsonl: a valid, unique task_id each, the fields a JSON object.
A task_id is a single folder name (validate_task_name) of at most MAX_TASK_ID_BYTES bytes, not
one of the files the rendered dataset root holds (RESERVED_TASK_IDS: tasks.jsonl, the dataset-level
files, prepare’s records and their lock files) and not hidden (a leading .), since task discovery skips hidden
folders.
Render every row of <template>/tasks.jsonl into <output>/<task_id>/ and return output.
Each rendered task.toml carries the row’s own [task].name, <org>/<task_id>
(rendered_task_toml), when the template has one. Everything in output is replaced, so a row removed
from tasks.jsonl leaves no stale folder behind, except prepare’s records (RECORD_FILES and their lock files), which stay: output is the dataset root
those records live in, and a built image or a resolved base image must survive the next prepare. Dataset-level
files are copied beside the task folders. The rows are rendered into a hidden sibling of output first and
swapped in afterwards (_install_rendered), under an exclusive lock on output’s hidden sibling lock file
(.<name>.lock) so that two commands rendering one template into one folder install one after the other: an
entry whose content did not change is left in place, so an identical re-render leaves every task folder present
throughout, and a render that fails leaves output as it was. Raises ParameterizedError (a
HarborTaskError) for a malformed row or template.
Substitute {{ task_data.key }} and {{ key }} with the row’s values; an unknown key is an error.
So is any other {{ ... }} still in the text once the placeholders are filled ({{ task_data.q.upper }},
{{ q | upper }}, {{ task-data.q }}): only the two spellings above are placeholders, and a misspelt one
would otherwise reach the agent, or solution/answer.md, as raw text. The check runs over the input text with
the placeholders removed, never over the substituted values, so a field whose value contains {{ renders. A
{{ that never closes ({{ a } directly before a valid placeholder, whose own }} would otherwise be
taken for its end) is refused the same way.
instruction.md for instruction.template.md; None for a file that is not a template.
The template’s task.toml for one row: [task].name rewritten to <org>/<task_id>, every other byte
kept; the text unchanged when it has no [task].name.
The rewrite is line-based (the name = "..." line under the [task] header) and checked: the result must
parse to the template’s document with only that name changed, so a name spelled some other way (an inline
task = { name = "..." } table, a dotted task.name key, a multi-line string) is a ParameterizedError
asking for the plain form rather than a mangled file. A template name that is not org/name, or a task_id
outside the characters Harbor admits in a name, is refused with the row named: Harbor’s own check would refuse the
published folder, and the loader would otherwise run a task Harbor cannot.
<org>/<task_id>: the Harbor task name of the row rendered from a template whose [task].name is given.