Skip to content

feat: add taskset selection views and select config - #2695

Merged
mikasenghaas merged 9 commits into
mainfrom
feat/taskset-select
Sep 28, 2026
Merged

mikasenghaas merged 9 commits into
mainfrom
feat/taskset-select

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add lazy, chainable selection views to Taskset: include and exclude (by idx, ids, keys or names), shuffle(seed), skip(n) and take(n). Each view is a shallow copy, so the base taskset does not change.
    taskset.include(idx=["0:100"]).exclude(names=["broken"]).shuffle(seed=0).take(5)
  • Add SelectConfig and TaskMatchConfig (verifiers/v1/configs/select.py). Taskset.select(config) chains the views in a fixed order: include, exclude, shuffle, skip, limit. idx takes ints and Python slices start:stop:step, each part optional ("100:", ":50", "::2", "0:10,17"); negative positions are not supported. Within include or exclude, a task matches when any list names it.
  • Give the eval, debug, validate and GEPA configs a top-level select block. The four entrypoints select only through Taskset.select, so the copied "shuffle, then head" blocks are gone.
  • Keep SelectConfig free of aliases, so that a config with several select blocks (prime-rl's group defaults and sources) has no ambiguous flags. SelectCLIConfig adds the short flags for the one select block of an entrypoint: -n sets select.limit and -s sets select.shuffle.
  • Make idx a framework field: iteration sets TaskData.idx to the task's position in load() and overwrites any value that the taskset sets. Iteration also applies the config system_prompt before the views, so a keys match compares the keys that traces record. The built-in tasksets, the example environments, the vf-init template and the docs no longer pass idx.
  • Add an optional durable TaskData.id.
  • Add Taskset.bounded. take bounds a view, and so does an include that names only closed idx ranges. shuffle, an include by ids, keys or names (which could wait forever for a match), and an exclude of an open idx range (which drops every later task) raise on a view that is not bounded. select applies limit after the shuffle, so there only closed include.idx ranges can bound it; the error says so. An idx-only include stops reading load() after its last position.
  • Log a warning when an include or exclude entry matches no task (usually a typo).
  • Document the views and the select block in docs/v1/tasksets.md#selecting-tasks.

Companion PRs:

Breaking

  • EvalConfig, DebugConfig and ValidateConfig drop num_tasks and shuffle. Use select.limit and select.shuffle (-n and -s still work). In TOML, change a top-level num_tasks = 5 to select.limit = 5.
  • GEPAConfig drops shuffle, and GEPA no longer shuffles by default. Pass -s to keep the old train/val split behaviour.
  • Taskset.head(n) becomes take(n). Taskset.view and the SEED constant are removed.
  • Tasksets that never set idx get new default task keys, because idx is part of the content hash. An interrupted eval of such a taskset cannot resume across this change.

Verification

  • uv run pytest tests -m "not e2e": 113 passed. ruff check and ruff format --check pass. tests/v1/test_taskset.py has one test per behaviour: idx assignment, each view, lazy reads, the bounds that allow shuffle, idx parsing, keys matches under a system_prompt override, the SelectConfig defaults and SelectCLIConfig aliases, and the fixed order of select().
  • vf-validate on a dummy 20-task taskset and a dummy infinite taskset: 27 CLI combinations of --select.*, -n, -s and a TOML [select] block matched an independent oracle. The combinations included every idx form, the union inside include, exclude.idx, and the infinite-taskset errors.
  • Real configs against a local mock OpenAI server (no inference spend). The mock counted the model calls:
    • vf-eval @ configs/gsm8k.toml: tasks 0–4 × 3 rollouts, 15 calls. With -s: the oracle's shuffled 5 of 1319.
    • With --select.include.idx 100:103 --select.exclude.idx 101: tasks 100 and 102, 6 calls.
    • vf-eval @ configs/wordle.toml (infinite): tasks 0–4.
    • vf-eval @ configs/alphabet_sort.toml -s: refused with "cannot shuffle", 0 calls. With --select.include.idx 0:4 -s -n 2: runs.
    • vf-validate gsm8k … and vf-debug reverse-text -n 3 -s: task order matches the oracle.
  • vf-eval @ configs/<each>.toml --dry-run for all 12 configs: each resolves to its intended select.limit.

🤖 Generated with Claude Code


Note

Medium Risk
Breaking config and task-key semantics affect eval resume and all CLIs; selection logic on infinite tasksets is easy to misconfigure but is guarded with explicit errors.

Overview
Introduces a unified [select] block (and --select.* / -n / -s) for choosing which tasks run, replacing top-level num_tasks and shuffle on eval, debug, validate, and GEPA. Taskset.select(SelectConfig) applies lazy views in fixed order: include / exclude (by idx slices, ids, keys, names), shuffle, skip, limit (take in Python; head is removed).

TaskData.idx is now set from the load() stream position during iteration (overwriting author-set values); built-in and example tasksets drop manual idx, and TaskData.id is added for durable source IDs. Taskset.bounded gates shuffle and identity-based filters on infinite tasksets; unmatched include/exclude entries log warnings.

Sample configs/*.toml move limits to [select].limit. Docs describe selection and updated task identity. GEPA no longer shuffles by default—use select.shuffle (-s) for the old split behavior. Task keys may change for tasksets that relied on embedded idx in the hash, affecting resume across this upgrade.

Reviewed by Cursor Bugbot for commit c305bde. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add lazy taskset selection views and select config replacing num_tasks

  • Iterating a taskset now assigns idx automatically, so all environment loaders drop manually supplied TaskData.idx; TaskData gains an optional durable id field and Task gains a with_data copy helper (taskset.py, task.py).
  • Taskset gains lazy include, exclude, shuffle, skip, and take views (replacing head), plus a select view that applies include/exclude/shuffle/skip/limit in a fixed order. Matching supports idx ranges, id, key, and name, warns on unmatched selectors, and rejects unbounded shuffles of infinite streams.
  • New SelectConfig/SelectCLIConfig models in select.py replace top-level num_tasks and shuffle in EvalConfig, DebugConfig, ValidateConfig, and GEPAConfig; CLI aliases map -n to select.limit and -s to select.shuffle. Exported via init.py.
  • Runners for eval, validate, debug, and GEPA now apply config.select and require the resulting view to be bounded; example TOML configs and docs updated.
  • Behavioral Change: config files using top-level num_tasks/shuffle no longer parse — migrate to [select] with limit/shuffle. head() on tasksets is replaced by take(); check configs/ and the GEPA runner's shuffle default (now off unless select.shuffle is set).

Macroscope summarized c305bde.

mikasenghaas and others added 3 commits September 27, 2026 19:31
Tasksets gain lazy, chainable views: include and exclude (by idx, id,
key or name), shuffle, skip and take. select(SelectConfig) chains them
in a fixed order and is the one selection path for the eval, debug,
validate and GEPA entrypoints, which now take a top-level `select`
block (`-n` sets select.limit, `-s` sets select.shuffle).

Iteration now sets TaskData.idx to the task's position in load(),
overwriting any value the taskset sets, so the built-in tasksets and
examples stop passing idx. TaskData gains an optional durable `id`.

Breaking: the eval, debug and validate configs drop `num_tasks` and
`shuffle` (use `select.limit` / `select.shuffle`), GEPA drops
`shuffle` and no longer shuffles by default, and Taskset.head() is
replaced by take(). Tasksets that never set idx get new default task
keys, as idx is part of the content hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas and others added 2 commits September 27, 2026 21:21
One test per behaviour: idx assignment, each view, lazy reads, the
bounds that allow shuffle, idx parsing, the select config defaults and
aliases, and the fixed order that select() applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the short flags on SelectConfig itself, every nested select block
(a group default, each source) claimed -n and -s, so a CLI with several
of them had ambiguous flags. SelectConfig is now alias-free, and
SelectCLIConfig adds -n/-s for the one select block of an entrypoint:
the eval, debug, validate and GEPA configs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread configs/agentic_judge.toml Resolved
Comment thread docs/v1/tasksets.md Resolved
Comment thread docs/v1/tasksets.md Resolved
Comment thread docs/v1/tasksets.md Resolved
Comment thread verifiers/v1/task.py Resolved
mikasenghaas and others added 2 commits September 27, 2026 22:00
`idx` entries now take the full `start:stop:step` slice syntax (no
negative positions, which would need the stream's length). The configs
use `[select]` tables, and the docs say idx is set automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iteration applied the config system prompt after the views, so an
include/exclude by key compared keys without the prompt while traces
record them with it; both now apply before the views. A zero step
spelled "00" passed validation and divided by zero. idx entries parse
into plain ranges, and the shuffle error names the bound that works
through select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikasenghaas
mikasenghaas marked this pull request as ready for review September 27, 2026 22:36
Comment thread verifiers/v1/taskset.py Resolved
Comment thread verifiers/v1/configs/select.py Resolved

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d664149. Configure here.

Comment thread verifiers/v1/taskset.py Resolved
@macroscopeapp

macroscopeapp Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a framework-wide task-selection capability and changes the eval/debug/validate/GEPA execution paths, task identity semantics, and public configuration APIs. The breaking migration and altered selection behavior have a broad enough runtime impact to warrant human review.

You can add or adjust custom eligibility rules. Learn more.

mikasenghaas added a commit to PrimeIntellect-ai/prime-envs that referenced this pull request Sep 28, 2026
## Summary

- `lean_common/scripts/compile_statements.py`: take the first `limit`
tasks with `itertools.islice` instead of `Taskset.head(limit)`.
PrimeIntellect-ai/verifiers#2695 removes `head()` (it becomes `take()`).
`islice` works with verifiers both before and after that change, so this
PR can merge on its own.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single-line behavioral change in a dev script; slicing semantics
should match the old head/limit behavior for taskset iteration.
> 
> **Overview**
> Updates **`compile_statements.py`** so `--limit` takes the first N
tasks via **`itertools.islice(taskset, limit or None)`** instead of
**`taskset.head(limit)`** (or iterating the full taskset when no limit).
> 
> This keeps the compile script working when upstream **verifiers**
drops **`head()`** in favor of **`take()`**, without requiring a
coordinated dependency bump.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ad833f2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- Macroscope's pull request summary starts here -->
<!-- Macroscope will only edit the content between these invisible
markers, and the markers themselves will not be visible in the GitHub
rendered markdown. -->
<!-- If you delete either of the start / end markers from your PR's
description, Macroscope will append its summary at the bottom of the
description. -->
> [!NOTE]
> ### Replace `taskset.head()` with `itertools.islice` in `statements`
> The `statements` function in
[compile_statements.py](https://github.com/PrimeIntellect-ai/prime-envs/pull/852/files#diff-99d68b7a3d3eedfe5da62e6d44b91ea9a797b6153420172eec83411fe947372a)
now slices the taskset with `itertools.islice(taskset, limit or None)`.
A positive limit takes at most that many tasks; a falsy limit takes the
whole iterable. Risk: a negative `limit` now raises `ValueError` from
`islice` instead of being handled by `head()`.
>
> <!-- Macroscope's review summary starts here -->
>
> <sup><a href="https://app.macroscope.com">Macroscope</a> summarized
ad833f2.</sup>
> <!-- Macroscope's review summary ends here -->
>
<!-- Macroscope's pull request summary ends here -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread docs/v1/tasksets.md Resolved
Comment thread docs/v1/tasksets.md Resolved
On an infinite taskset, include by ids, keys or names followed by
take(n) read forever when fewer than n tasks matched (e.g. a typo), so
include now raises on an unbounded view for identity matches. An idx
slice like ":0" passed validation, selected nothing and still built
task 0; an omitted start now counts as 0 in the empty-range check. The
docs use the long flag forms and name the shorthands in comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread verifiers/v1/taskset.py Resolved
On an infinite taskset, exclude(idx="5:") drops every task past
position 5, so a later take(n) waited forever. exclude now raises on
an unbounded view when an idx slice covers the whole tail; a slice
with a step above 1 still leaves tasks and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikasenghaas
mikasenghaas merged commit e5a9e17 into main Sep 28, 2026
14 checks passed
@mikasenghaas
mikasenghaas deleted the feat/taskset-select branch September 28, 2026 20:05
mikasenghaas added a commit to PrimeIntellect-ai/prime-rl that referenced this pull request Sep 28, 2026
## Summary

- Give every train and eval source a `select` block (`EnvConfig.select`,
a `vf.SelectConfig`): `include`/`exclude` by task `idx`, `ids`, `keys`
or `names`, then `shuffle`, `skip` and `limit`. The orchestrator loads
each source's tasks with `vf.load_taskset(...).select(source.select)`.
prime-rl no longer shuffles or slices tasksets itself, so it picks the
same tasks as `vf-eval` for the same selection.
  ```toml
  [[orchestrator.train.source]]
  env.taskset.id = "gsm8k"
  select.exclude.idx = [":100"]   # hold out the first 100 tasks

  [[orchestrator.eval.source]]
  env.taskset.id = "gsm8k"
  select.include.idx = [":100"]
  ```
- Every source group (`[orchestrator.train]`, `[orchestrator.eval]`, SFT
`[eval]`, the top level of `uv run eval`) takes a shared `select` block.
The same before-validator that layers the group `env` block layers it
under each source, field by field, and a field that the source sets
wins. A source's `include` or `exclude` replaces the group's whole
block. A source `select` that does not validate keeps its own block, so
its error surfaces. For example, this evaluates 128 tasks of each
taskset:
  ```toml
  [orchestrator.eval.select]
  limit = 128
  ```
- Only `uv run eval` has the `-n`/`-s` short flags
(`select.limit`/`select.shuffle`), through `vf.SelectCLIConfig`. The
`rl`, `orchestrator` and `sft` CLIs have several `select` blocks, so
they get no short flags.
- The dashboard derives the expected episode count from `select.limit`,
per source and for vf-shaped configs.
- Migrate the configs, examples and docs from `num_examples` to
`select.limit`. Group-level blocks use `[... .select]` tables.
- Update the eval docs and skill: resume no longer checks the config
(#3690), and a resume matches landed episodes to the new `select` by
`task.key`.
- Update the `configs` skill (`[select]`, `-s`). Update the eval skill:
the `select` shorthands, and common patterns (a random subset, tasks by
position or name, dropping broken tasks, contiguous and random
train/test splits via `include`/`exclude` and `skip`).
- Pin `deps/verifiers` to its main, which merged
PrimeIntellect-ai/verifiers#2695 (`Taskset.select`, `vf.SelectConfig`).
Raise the `verifiers` minimum to `0.3.2.dev152`, its first release, in
both `pyproject.toml` files, so the slim `prime-rl-configs` install gets
it.

Companion PRs: PrimeIntellect-ai/verifiers#2695 (merged),
PrimeIntellect-ai/prime-envs#852 (independent; `compile_statements`
stops calling `head()`).

## Breaking

- `EnvConfig.shuffle` is removed. Use `select.shuffle`.
- `EvalSourceConfig.num_examples` and the group-level `num_examples` are
removed. Use `select.limit` on the source or on the eval block.
- The shuffle seed changes from 42 to verifiers' default 0
(`select.seed`). Existing `shuffle = true` configs pick other tasks, so
eval numbers before and after this change are not comparable.
- Default task keys change for tasksets that never set `idx` (see
verifiers#2695). An interrupted eval of such a taskset cannot resume
across this change.

## Verification

- The slim-install CI check, run locally: the `prime-rl-configs` wheel
installed outside the workspace resolves `verifiers 0.3.2.dev152`, and
the workflow's import script passes.
- `uv run pytest tests/unit/test_configs.py tests/unit/eval
tests/unit/orchestrator tests/unit/test_dashboard_timeline.py`: 296
passed (2 vLLM-only tests deselected; `vllm` is not installed on the
test box). `test_load_configs` loads every migrated TOML.
- Real configs and CLI calls against a local mock OpenAI server (no
inference spend). The mock counted the model calls, and the selected
task `idx` matched an independent oracle:
- `uv run eval @ configs/debug/eval/single-turn.toml`: tasks 0–7 × 2, 16
calls.
- `uv run eval @ configs/debug/eval/resume.toml`: first run 16 calls.
`--resume --select.limit 10` restored 16 episodes and ran only tasks 8–9
(4 calls). `--resume --select.include.idx 0:5` kept the landed episodes
of tasks 0–4 and made no model calls.
- `uv run eval gsm8k -n 4 -s --select.exclude.idx '[0]'`: the oracle's 4
tasks, 4 calls.
- A two-source TOML that splits gsm8k into `[:50]` and the rest under a
group `select` default: both sources match the oracle, 5 calls.
- Config checks: train and eval group `select` defaults reach every
source, a source's own fields win, and the resolved `OrchestratorConfig`
re-validates to the same values. `uv run rl -h`, `orchestrator -h` and
`sft -h` list no `-n`/`-s`; `uv run eval -h` does.
- `uv run rl --dry-run` on `configs/debug/algo/grpo.toml`,
`examples/basic/reverse-text/rl.toml` and
`configs/debug/multi-env/rl.toml`, and `uv run sft --dry-run` on
`examples/basic/reverse-text/sft.toml`: group eval limits (128, 64, 32)
resolve onto each source, a per-source `select.limit = 256` is kept, and
train sources default to all tasks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Breaking config renames and verifiers-driven
selection/shuffle/task-key semantics affect every train/eval run and
eval resume comparability across the upgrade, though runtime logic is
largely delegated to verifiers.
> 
> **Overview**
> Replaces **`num_examples`** and per-source **`shuffle`** with
verifiers' **`select`** block on every train/eval source (and
group-level `[select]` / `[orchestrator.eval.select]` /
`[eval.select]`). **`select.limit`** caps how many tasks run;
**`include`/`exclude`**, **`shuffle`**, **`skip`**, and **`seed`**
follow verifiers' ordering so prime-rl matches **`vf-eval`** for the
same selection.
> 
> The orchestrator now loads tasks with
**`vf.load_taskset(...).select(source.select)`** instead of client-side
shuffle (seed 42) and **`islice`**. Group config merging layers shared
**`select`** onto each source like **`env`**. **`uv run eval`** maps
**`-n`/`-s`** to **`select.limit`/`select.shuffle`**; eval resume no
longer validates config against the interrupted run—landed episodes are
matched by **`task.key`** to the new **`select`**.
> 
> All example/debug/CI TOMLs, docs, and skills are migrated.
**`verifiers`** minimum is raised to **`0.3.2.dev152`**. The dashboard
expects **`select.limit`** when estimating episode counts.
> 
> **Breaking:** rename configs from **`num_examples`** →
**`select.limit`**, **`shuffle`** → **`select.shuffle`**; default
shuffle seed and task keys may differ from pre-change runs
(resume/comparability implications).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f30798a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants