Skip to content

feat(v1): client-side tasksets + stateless env server (v1-only)#2039

Open
mikasenghaas wants to merge 7 commits into
mainfrom
feat/client-side-tasksets
Open

feat(v1): client-side tasksets + stateless env server (v1-only)#2039
mikasenghaas wants to merge 7 commits into
mainfrom
feat/client-side-tasksets

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Rebased onto the multi-agent episode wire (#1939): origin/main was merged wholesale and the change re-landed on top, adapted to Env/Episode.

  • The v1 env server no longer owns a taskset. The client — the eval entrypoint here, the prime-rl orchestrator in the companion PR — loads the taskset once and ships each dispatched task's data on the run request (task_data, the task's dumped TaskData); the server pydantic-validates it into the taskset's declared TaskData type and rebuilds the Task exactly as load() would, then runs it through the env (slots/run_slot) as usual.
  • run_eval_server now selects and resumes tasks client-side, in the same coordinate system as the in-process runner (task.data.idx) — fixing the resume coordinate bug structurally (supersedes fix(v1): resume must match saved rollouts by load position, not data.idx #2017, see Verification).
  • Server-side task state is gone: no per-worker load() (a pool of N workers used to pull the dataset N times), no idx-addressed task cache, no MAX_LAZY_TASKS, and infinite tasksets no longer need to generate identical sequences in every pool worker — the one client-side generator is pulled lazily and each produced task is shipped to whichever worker runs it.
  • Task data serializes whole: a TaskData subclass may not exclude fields from serialization (enforced at class definition) — an excluded field would vanish on the wire and the server would rebuild the task with silently-defaulted values. Harbor's task_dir, the only such field in the tree, now serializes (rows also become rebuildable for replay).
  • Taskset.task_type() classmethod resolves the declared Task subclass off the generic (used by the server, Env.__init__, and loaders.task_type).
  • The legacy v0 bridge keeps task_idx addressing — its dataset genuinely lives server-side — and run_group stays a legacy-only route, untouched. info.num_tasks is now only meaningful there.

Verification

Repro taskset: load() filters a raw 5-row dataset (like LeanTaskset filtering empty statements), so data.idx is {0,1,3,4} while load positions are {0,1,2,3}.

Before (main): uv run eval gappy_echo_v1 --server -n 4 -r 1 completes all 4 tasks; uv run eval --resume <dir> then reports "1 task(s), 1 rollout(s) owed", deletes the good idx=4 record, and re-runs idx=3traces.jsonl ends up {0,1,3,3} (a duplicate and a silently lost task).

After (this branch, on the episode wire):

  • resume of the complete run prints nothing to resume … all 4x1 rollouts already completed without error, episodes untouched ({0,1,3,4}, all ok);
  • deleting the idx=3 episode and resuming re-runs exactly data.idx 3 and restores {0,1,3,4}.

Companion prime-rl training on this branch pair: 5-step RL runs of reverse-text-v1 (finite) and alphabet-sort-v1 (infinite) — see the companion PR's Verification.

uv run pytest tests/v1 -m 'not e2e' passes.

Breaking

  • Env-server wire protocol: v1 run requests take task_data (the task's wire data) instead of task_idx; task_idx remains only for the legacy bridge. EnvClient.run signature changed accordingly. Migration: load the taskset in the client (vf.load_taskset(...).select(...)) and pass task_data=task.data.model_dump(mode="json").
  • TaskData fields can no longer set exclude=True — class definition now raises. Task data must survive the wire whole; harbor's task_dir (the only affected field) is un-excluded and now appears in saved traces.
  • InfoResponse.num_tasks is None for v1 servers (the client owns the taskset and its count); only the legacy bridge reports a count.
  • A client of an env server must now be able to load the taskset locally (the taskset package installed client-side); the server no longer materializes tasks for it.

Companion PR: PrimeIntellect-ai/prime-rl#3043.

🤖 Generated with Claude Code

The env server no longer owns a taskset: the client (eval entrypoint, prime-rl
orchestrator) loads the taskset once and ships each dispatched task's data on the
run request; the server pydantic-validates it into the taskset's declared TaskData
type and rebuilds the Task exactly as load() would.

This removes the server-side task cache (and MAX_LAZY_TASKS), the requirement that
infinite tasksets generate identical sequences in every pool worker, and the
duplicate dataset load per worker. It also fixes resume-after-interrupt through the
server path structurally: the client now dispatches and resumes in one coordinate
system (task.data.idx), so tasksets whose load() filters rows (gappy idx) no longer
rerun the wrong tasks and drop good traces (supersedes #2017).

The legacy v0 bridge keeps task_idx addressing — its dataset genuinely lives
server-side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mikasenghaas and others added 4 commits July 15, 2026 21:31
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…classes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d fields

The dispatch payload is a plain model_dump: a TaskData subclass may not exclude
fields (enforced at class definition), since an excluded field would vanish on the
wire and the server would rebuild the task with silently-defaulted values. Harbor's
task_dir — the only excluded field — now serializes (a host path in saved traces is
harmless, and rows become rebuildable for replay).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat(v1): client-side tasksets — the env server runs tasks it is sent feat(v1): client-side tasksets + stateless env server (v1-only) Jul 15, 2026
@mikasenghaas
mikasenghaas marked this pull request as ready for review July 15, 2026 22:13
@macroscopeapp

macroscopeapp Bot commented Jul 15, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a significant architectural change moving tasksets from server-side to client-side ownership. Additionally, an unresolved high-severity bug was identified where tasks sharing the same idx cause incorrect data dispatch due to dict key collision.

You can customize Macroscope's approvability policy. Learn more.

samsja
samsja previously approved these changes Jul 16, 2026
Comment thread verifiers/v1/cli/eval/runner.py Outdated
idxs = sample(list(range(info.num_tasks)), config.shuffle, config.num_tasks)
payloads: dict[int, dict] = {idx: {"task_idx": idx} for idx in idxs}
else:
group_scored = bool(tasks) and bool(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's wait for #1939 to make group rewards obsolete?

mikasenghaas and others added 2 commits July 22, 2026 00:18
…t-side tasksets re-land on top

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same design as before the multi-agent merge, adapted to episodes: the client
(eval entrypoint, prime-rl orchestrator) loads the taskset once and ships each
dispatched task's data on the run request; the server validates it into the
taskset's declared TaskData type and rebuilds the Task exactly as load() would.
The server keeps no task state (no per-worker load(), no idx cache, no
MAX_LAZY_TASKS), and run_eval_server dispatches and resumes in data.idx — the
same coordinate system as the local runner. TaskData subclasses may not exclude
fields (rejected at class definition; harbor's task_dir un-excluded). The legacy
bridge keeps task_idx addressing; run_group is untouched (legacy-only route).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
else:
group_scored = False
idxs = [task.data.idx for task in tasks]
payloads = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High eval/runner.py:180

When two selected tasks share the same task.data.idx (including the valid default None), the payloads dict at line 180 keeps only the last task's serialized data, and every entry in idxs dispatches that same data via run_unit. Distinct tasks that collided are silently replaced, so the eval reports results for the wrong task set. This happens because payloads is keyed solely by idx, which is optional and not guaranteed unique by Taskset.select. Consider keying the dispatch by the task object itself (or a unique position) instead of idx.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/cli/eval/runner.py around line 180:

When two selected tasks share the same `task.data.idx` (including the valid default `None`), the `payloads` dict at line 180 keeps only the last task's serialized data, and every entry in `idxs` dispatches that same data via `run_unit`. Distinct tasks that collided are silently replaced, so the eval reports results for the wrong task set. This happens because `payloads` is keyed solely by `idx`, which is optional and not guaranteed unique by `Taskset.select`. Consider keying the dispatch by the task object itself (or a unique position) instead of `idx`.

mikasenghaas added a commit to PrimeIntellect-ai/prime-rl that referenced this pull request Jul 22, 2026
Companion to the rebased PrimeIntellect-ai/verifiers#2039. A v1 env's taskset is
loaded once, in the orchestrator: finite tasksets become the train source's
shuffled example table (real tasks, not index ranges); an infinite taskset's
generator is pulled per example. Each dispatched env-rollout ships its task's
data (task.data.model_dump()) and the env server validates and runs it. The
legacy (v0) bridge keeps its server-side dataset, task_idx addressing, and the
run_group route.

Co-Authored-By: Claude Fable 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.

3 participants