Skip to navigation

nemo_gym.data.tasks.harbor.parameterized

View as Markdown

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

NameDescription
ParameterizedErrortasks.jsonl or a template file cannot be rendered.
TaskRow-

Functions

NameDescription
_install_renderedSwap the entries of fresh into output: stale entries go (records stay), unchanged task folders and files
_move_into_placeRename fresh to target, whatever is there now: a file over a file atomically, otherwise the old entry is
_refuse_task_data_twinsRefuse a task_data.json (or a task_data.template.json) at the template root.
_remove-
_render_treeRender source into target: templates filled, every other entry copied with its mode.
_same_treeWhether two rendered trees hold the same entries, byte for byte and mode for mode (no links in either).
is_harbor_nameWhether name is an org/name Harbor accepts for a task or a dataset (its ORG_NAME_PATTERN, no ..).
is_record_fileA record prepare writes at the dataset root, its lock file, or a record writer’s in-flight temporary: kept when
is_template_folderA task folder with tasks.jsonl beside its task.toml.
read_rowsThe rows of &lt;template&gt;/tasks.jsonl: a valid, unique task_id each, the fields a JSON object.
render_template_folderRender every row of &lt;template&gt;/tasks.jsonl into &lt;output&gt;/&lt;task_id&gt;/ and return output.
render_textSubstitute &#123;&#123; task_data.key &#125;&#125; and &#123;&#123; key &#125;&#125; with the row’s values; an unknown key is an error.
rendered_nameinstruction.md for instruction.template.md; None for a file that is not a template.
rendered_task_tomlThe template’s task.toml for one row: [task].name rewritten to &lt;org&gt;/&lt;task_id&gt;, every other byte
task_name_for&lt;org&gt;/&lt;task_id&gt;: the Harbor task name of the row rendered from a template whose [task].name is given.

Data

DATASET_FILES

MAX_TASK_ID_BYTES

RECORD_FILES

RESERVED_TASK_IDS

SKIPPED_FILES

TASKS_FILE

TEMPLATE_MARKER

_HARBOR_NAME

_LEFTOVER

_NAME_LINE

_PLACEHOLDER

_TABLE_HEADER

API

class nemo_gym.data.tasks.harbor.parameterized.ParameterizedError()

Bases: HarborTaskError

tasks.jsonl or a template file cannot be rendered.

class nemo_gym.data.tasks.harbor.parameterized.TaskRow(
task_id: str,
task_data: dict[str, typing.Any]
)
Dataclass
task_data
dict[str, Any]
task_id
str
nemo_gym.data.tasks.harbor.parameterized._install_rendered(
fresh: pathlib.Path,
output: pathlib.Path
) -> None

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.

nemo_gym.data.tasks.harbor.parameterized._move_into_place(
fresh: pathlib.Path,
target: pathlib.Path
) -> None

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.

nemo_gym.data.tasks.harbor.parameterized._refuse_task_data_twins(
template: pathlib.Path
) -> None

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.

nemo_gym.data.tasks.harbor.parameterized._remove(
path: pathlib.Path
) -> None
nemo_gym.data.tasks.harbor.parameterized._render_tree(
source: pathlib.Path,
target: pathlib.Path,
fields: typing.Mapping[str, typing.Any],
task_id: str,
skip: set[str],
at_root: bool = True
) -> None

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).

nemo_gym.data.tasks.harbor.parameterized._same_tree(
a: pathlib.Path,
b: pathlib.Path
) -> bool

Whether two rendered trees hold the same entries, byte for byte and mode for mode (no links in either).

nemo_gym.data.tasks.harbor.parameterized.is_harbor_name(
name: object
) -> bool

Whether name is an org/name Harbor accepts for a task or a dataset (its ORG_NAME_PATTERN, no ..).

nemo_gym.data.tasks.harbor.parameterized.is_record_file(
name: str
) -> bool

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.

nemo_gym.data.tasks.harbor.parameterized.is_template_folder(
path: pathlib.Path
) -> bool

A task folder with tasks.jsonl beside its task.toml.

nemo_gym.data.tasks.harbor.parameterized.read_rows(
template: pathlib.Path

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.

nemo_gym.data.tasks.harbor.parameterized.render_template_folder(
template: pathlib.Path,
output: pathlib.Path
) -> pathlib.Path

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.

nemo_gym.data.tasks.harbor.parameterized.render_text(
text: str,
fields: typing.Mapping[str, typing.Any],
where: str
) -> str

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.

nemo_gym.data.tasks.harbor.parameterized.rendered_name(
name: str
) -> str | None

instruction.md for instruction.template.md; None for a file that is not a template.

nemo_gym.data.tasks.harbor.parameterized.rendered_task_toml(
text: str,
task_id: str,
where: str
) -> str

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.

nemo_gym.data.tasks.harbor.parameterized.task_name_for(
template_name: str,
task_id: str
) -> str

<org>/<task_id>: the Harbor task name of the row rendered from a template whose [task].name is given.

nemo_gym.data.tasks.harbor.parameterized.DATASET_FILES = (DATASET_FILE, TASK_DATA_MODULE, VERIFIER_FILE)
nemo_gym.data.tasks.harbor.parameterized.MAX_TASK_ID_BYTES = 255
nemo_gym.data.tasks.harbor.parameterized.RECORD_FILES = (MANIFEST_FILE, IMAGE_CONFIGS_FILE, BUILT_IMAGES_FILE)
nemo_gym.data.tasks.harbor.parameterized.RESERVED_TASK_IDS = frozenset((TASKS_FILE, *DATASET_FILES, *RECORD_FILES, *((name + RECORD_LOCK_SUFF...
nemo_gym.data.tasks.harbor.parameterized.SKIPPED_FILES = (TASKS_FILE, 'README.md', *RECORD_FILES)
nemo_gym.data.tasks.harbor.parameterized.TASKS_FILE = 'tasks.jsonl'
nemo_gym.data.tasks.harbor.parameterized.TEMPLATE_MARKER = '.template'
nemo_gym.data.tasks.harbor.parameterized._HARBOR_NAME = re.compile('^[a-zA-Z0-9][a-zA-Z0-9._-]*/[a-zA-Z0-9][a-zA-Z0-9._-]*$')
nemo_gym.data.tasks.harbor.parameterized._LEFTOVER = re.compile('\\{\\{.*?\\}\\}|\\{\\{[^\\n]*', re.DOTALL)
nemo_gym.data.tasks.harbor.parameterized._NAME_LINE = re.compile('^(\\s*name\\s*=\\s*)("[^"\\\\]*"|\\\'[^\\\'\\\\]*\\\')(\\s*(?:#.*)?)...
nemo_gym.data.tasks.harbor.parameterized._PLACEHOLDER = re.compile('\\{\\{\\s*(?:task_data\\.)?([A-Za-z_][A-Za-z0-9_]*)\\s*\\}\\}')
nemo_gym.data.tasks.harbor.parameterized._TABLE_HEADER = re.compile('^\\[\\[?\\s*([^\\]]*?)\\s*\\]\\]?\\s*(?:#.*)?$')