Repository navigation
prime-runs - #2415
prime-runs#2415
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RpNehr64sniVfqKo62GKPw
`run_config()` now carries what the dashboard reads unconditionally — `model`, `num_examples` (the selected task count, passed in by the runner; -1 for "all" when unknown) and `rollouts_per_example` — alongside the `exclude_unset` dump, which drops defaults and so left the evals list showing "Unknown Model" and "0 examples". Credentials are stripped producer-side, so nothing sensitive reaches `metadata`: every value under a `headers` table and any value whose key names a credential (`api_key`, `Authorization`, `hf_token`; references such as `api_key_var` are kept) is masked in the structured dump, and the launched config file is uploaded verbatim only when parsing it (TOML/JSON) finds no credential-holding key — otherwise, or when it cannot be inspected, it is left out with a warning naming the offending paths. `open_run` honours `PRIME_RUNS_MODE=disabled`, which the explicit `mode="online"` used to override. A run that stays local keeps its own uuid instead of adopting the SDK's `disabled-…` placeholder. The E2E fixture sets `push=False`: `run_eval` opens a platform run before the first rollout when a key is present, and CI has one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CyW4j57juAg5cheTX8PmUR
The only places an eval config can carry a credential are a client's `headers` table and a seat's `harness.env` table: the API key itself is `api_key_var` (an env-var name) and `forward_env` exists so harness secrets need not be written down. Mask everything under those two tables and skip the verbatim `config_source` when the file sets either, instead of guessing from key names. Drops the credential-key regex, its reference-key exemption list and the dotted-path scan. Also widen `_close(summary=)` to `Mapping` so `metrics.RunSummary` (a TypedDict) type-checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDrFiuDUjdJrv8Qu4oud8C
…on't flip import order Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDEqDAXcNvbJ2GqsWttDhy
…ckouts don't flip import order" This reverts commit cca4860.
Upload only the three v0 metadata keys the dashboard reads (model, num_examples, rollouts_per_example), as the pre-SDK push did. The exclude_unset config dump and the launched file (config_source) are deferred to a follow-up: both can carry credentials (client.headers, harness.env), and the masking that made them safe was most of this module. This also keeps metadata.env a string, so the evals list no longer needs platform#4740 before this can ship. Removed: scrub_secrets, credential_tables and their helpers, RunConfig.source/record_source, and config_file_ref. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
Shorter docstrings and comments throughout; the PushState footer no longer reports per-sink losses (the SDK logs them); PushState is always constructed so the platform helpers take it unconditionally; run_config is inlined into open_run with num_examples required; build_samples is re-exported with a noqa instead of __all__. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
close() waited for in-flight cancel senders with `while self._cancel_tasks: await gather(*self._cancel_tasks)`. On 3.12+ a gather over tasks that are all already done completes eagerly without yielding to the loop, so the discard callbacks that empty the set never run and the coroutine spins at 100% CPU. It was latent on main, where close() is reached while the senders are still pending; the prime-runs integration waits for the rollouts to unwind first, so every Ctrl-C on a served eval hung. Gather only the pending tasks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
`adopt_id` existed only because prime-runs kept a record's own `run` block, so the platform's id had to be written into every episode from here. The SDK now sets `run.id`/`run.type` on the uploaded copy itself (keeping `name`), so `RunConfig.id` stays the local uuid in every mode and nothing in the eval CLI learns the platform's id. Resumed episodes are uploaded as they are on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHMjWstGWd1oGKjoj5RgeT
The run no longer mints a uuid of its own. `open_run` gives `RunConfig` the SDK run's id in every mode (the platform's evaluation id online, the SDK's local id otherwise), so the run dir, every trace on disk and the dashboard share one id. Linking uploads to the platform run stays the SDK's job (it re-keys every record it sends); this only decides what the local records say. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHMjWstGWd1oGKjoj5RgeT
## Why
Eval and training jobs need one call that opens a run on the platform, streams records while they execute, and marks the run finished, failed, cancelled, or crashed when they stop. verifiers and prime-rl each hand-roll that lifecycle against the raw API today — two copies of the same thing — and nobody uploads the config a run was actually launched with, which is why the Config tab shows a v0 blob instead of anyone's `eval.toml`.
## What
A new leaf package, `prime-runs` (`httpx` + `prime-traces`; `pyarrow` behind a `train` extra), where the run is an object:
```python
import prime_runs as pr
run = pr.init(name="gsm8k-qwen3-8b", environments=["gsm8k"],
model=config.model, framework="verifiers",
config="eval.toml") # stored byte-for-byte
print(run.url)
for episode in rollouts:
run.log_episodes([episode]) # a queue put; streams on a background thread
run.finish(summary=pr.metrics.from_episodes(episodes))
```
Training runs use the same handle over the RFT external-runs API (folded in from #873):
```python
run = pr.init(kind="train", model=base_model, environments=[env_id],
training=pr.TrainingSpec(max_steps=1000, batch_size=64),
config=cfg.model_dump(), team_id=team) # or id=os.environ["RUN_ID"] to attach
run.log_episodes(step_episodes) # every 10th step -> Parquet presign / PUT / confirm
run.log_metrics(metrics, step=step) # POST /rft/metrics, on its own uploader
run.finish() # idempotent finalize; failed/cancelled/crashed -> status PUT
```
`Run` exposes `id`, `url`, `kind`, `log_episodes()` / `log_traces()` / `log_metrics()`, `update_summary()`, `flush()`, `finish()`, `fail()`, a context manager (an exception → `failed`, Ctrl-C → `cancelled`, an exit without `finish()` → `crashed` via atexit), and loss counters (`dropped_records`, `failed_records`, `errors`).
Records are keyed to the run at upload: the traces sink sets `run.id` / `run.type` on the uploaded copy of every trace and episode (member traces included) and keeps the producer's `name` / `work`, so verifiers and prime-rl keep their own local run ids and never learn the platform's.
Every record is written to two places during the transition: Prime Traces (the system of record; an account outside the beta has that sink retire quietly) and the table today's viewer reads — the v0 sample table for evals, per-step Parquet objects for training. Once the viewers read traces natively, the legacy sink is one line to drop and no producer changes.
How it degrades: queued batches coalesce so the request rate tracks upload latency; a full queue drops (counted) rather than stalling the run; a transient failure costs its batch and three in a row retire the sink — a training run pauses it for five minutes instead, since it outlives any platform deploy; non-finite numbers become `null` in the sample tables rather than losing the row. `POST /evaluations/`, `/rft/external-runs` and `/samples` are never replayed after an ambiguous failure (a retry could duplicate); presign, confirm, `/rft/metrics` (one row per step server-side) and `/rft/finalize` are.
Integrations: [verifiers#2415](PrimeIntellect-ai/verifiers#2415) (eval), [prime-rl#3459](PrimeIntellect-ai/prime-rl#3459) (training).
## Known gaps
- **No producer-facing way to mark an evaluation FAILED** in the API as deployed today: `finalize` only moves PROCESSING → COMPLETED, so a crashed or interrupted eval stays RUNNING on the dashboard. The SDK records status/error under `metadata.prime_runs` and warns. platform#4740 adds `PUT /evaluations/{id} {status: FAILED|CANCELLED, error_message}` with a 409 guard; once deployed, `EvalsBackend.finalize` should use it and keep the metadata fallback for older backends (`RunStatus.CANCELLED` already exists for that).
- **`config_source` is stored, not yet shown.** `init(config=<path>)` keeps the launched file byte-for-byte under `metadata.config_source` (`run_config.config_source` for training). The eval Config tab renders it once platform#4740 lands; verifiers#2415 sends only the three v0 keys for now and adds the file in a follow-up, and the training Config tab needs the same branch.
- **Attach targets external runs.** `init(kind="train", id=...)` works for runs created through `POST /rft/external-runs`; the v1 monitoring endpoints answer 400 for a hosted run's id (the same endpoints prime-rl's old monitor used, so nothing regresses).
- **Training samples are keyed by dispatch step.** Objects are additive per step on the platform, so an off-policy episode landing in a later batch adds an object for its dispatch step; each object numbers its `sample_id`s in its own range (a forked child in a random band). "Step N" in the sample viewer means dispatched at N, while the metrics at N describe the batch trained at N.
## Before merge
1. ~~prime-traces 0.0.3 on PyPI (#872), then `prime-traces>=0.0.3` here.~~ Done.
2. A PyPI **pending publisher** for `prime-runs` (owner `PrimeIntellect-ai`, repo `prime`, workflow `release-runs.yml`, environment `pypi-prod`). The name is unclaimed; the first run of the release workflow creates the project. The workflow publishes before it tags, so a failed first publish leaves no tag and re-runs from `main` via `workflow_dispatch`.
3. ~~Coalesce queued sample batches, add `RunStatus.CANCELLED`, fold #873 in so 0.1.0 carries the training backend.~~ Done.
4. Squash-merge (`main` requires linear history); `release-runs.yml` publishes 0.1.0 with eval + train. verifiers#2415 and prime-rl#3459 then drop their `[tool.uv.sources]` git pins and relock.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> New client code talks to production eval/RFT/traces APIs with nuanced retry and upload semantics; mistakes could duplicate runs, drop samples, or mis-report terminal status, though behavior is heavily tested and defaults to warn-not-raise.
>
> **Overview**
> Introduces **`prime-runs`**, a new leaf Python package (`httpx` + `prime-traces`, optional `pyarrow` for training) that centralizes platform run lifecycle: **`init()`** registers or attaches to a run, **`log_episodes` / `log_traces` / `log_metrics`** stream records on background uploaders, and **`finish()`** drains queues and closes the run (with context-manager, atexit crash reporting, and fork-safe HTTP/traces clients).
>
> **Eval** runs use **`EvalsBackend`** (`/evaluations`, hub environment resolve) and dual sinks to Prime Traces plus the legacy v0 sample table; **training** runs use **`RftBackend`** (`/rft/external-runs`, attach via `id=`), Parquet sample uploads on a step cadence, and a separate metrics uploader. The SDK stamps platform **`run.id` / `run.type`** on records, stores launch **`config_source`** verbatim, and implements deliberate retry/idempotency (no replay on ambiguous `POST` creates or append-only samples).
>
> **Repo wiring:** CI gains **`test-runs`** (Python 3.11–3.13) and version-bump enforcement for `packages/prime-runs`; **`release-runs.yml`** publishes to PyPI and tags `prime-runs-v*`. Hermetic tests cover HTTP, backends, sinks, projection, and end-to-end `init()` flows.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1b65b9a. 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 Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
prime-runs 0.1.0 is published (prime#856 → prime-runs-v0.1.0), so the TEMPORARY git source is gone; `prime-runs>=0.1.0` now resolves from PyPI and the lock moves prime-traces from the git 0.0.2 to PyPI 0.0.3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a new Prime Runs integration and changes default evaluation behavior by opening, streaming, and finalizing platform runs throughout execution. The external side effects, lifecycle/identity changes, and unresolved run-ID ownership design question warrant human review. You can add or adjust custom eligibility rules. Learn more. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9955f74c4b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ffort - `abort_run` maps Ctrl-C / task cancellation to `RunStatus.CANCELLED` (was CRASHED), matching the SDK's own `__exit__` and the platform's producer-closable vocabulary. - `log_episodes` wraps `run.log_episodes` so the hand-off is best effort like `open_run` and `_close` already are: the SDK keeps upload failures on its uploader thread under `on_error="warn"`, so this only guards the call itself, but a platform problem must never fail a rollout or leave the run open. Both runner call sites (resume replay, `on_complete`) use it. Verified live: a two-task eval interrupted mid-rollout closes out `cancelled` in ~4 s; one interrupted after the last rollout stays `completed` with both samples. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
`_close` used to leave `PushState.error` unset whenever `run.finish()` returned, so the footer read `Traces pushed` even when the SDK's uploader had dropped records (queue overrun) or a sink had failed a batch. Read `run.failed_records` / `run.dropped_records` after the close-out into `PushState.incomplete`, log it, and render it as a red suffix on the `Traces pushed` line: the run exists and holds what did land. A sink that switched itself off quietly (Prime Traces outside the beta) is not counted by the SDK and so does not redden the footer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
Picks up prime-runs 0.1.1 (prime#896): the platform client normalises only the URL path when recognising an API root and sends its auth headers per request as well as on the pool. No behaviour change for the eval path; the relock moves nothing else. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
|
Re-triggering CI: GitHub dropped the workflow trigger for 65be671 (only CodeQL ran). Reopening immediately. |
Resolves `verifiers/v1/serve/client.py` in main's favour: #2518 fixes the same eager-gather spin in `EnvClient.close()` that this branch had patched (pop each cancel task before awaiting it), so the branch's hunk is dropped. `uv lock` re-run on the merged tree; prime-runs stays 0.1.1, prime-traces 0.0.3, and main's Python 3.14 bump carries through. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e06c307. Configure here.
Once `finish_run` has started, a Ctrl-C cancels only the runner's await; the worker thread completes the run regardless, and the SDK serialises `finish()` so the first caller decides the status. Routing that interrupt through `abort_run` started a second, no-op `finish()` that then logged `--push: cancelled` for a run that had completed. The runner now skips `abort_run` once close-out has begun, and `_close` logs the run's own status rather than the one it asked for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
A Ctrl-C while the close-out was still queued on the executor could cancel it before it ran: `closing` had already been set, so `abort_run` was skipped too and the run was left for the atexit hook to report as crashed. The close-out now runs under `run_shielded`, which awaits it to completion and re-raises the cancellation after, so the flag goes. verifiers' first Ctrl-C raises KeyboardInterrupt inside the loop rather than cancelling the task, so `abort_run` can still be reached while `finish_run` is mid-flight on its thread. The SDK serialises `finish()` and sets `run.status` as one starts; `abort_run` now stands down for a run whose status is no longer running, instead of adding a no-op second finish and a duplicate log line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYdQmKAVnGteT1tkimJb3g
Main moved the eval CLI's platform upload onto prime-runs (#2415, #2569, #2577) and taught the runner `run.attach`. All of that lives in files this branch deletes, so the merge keeps the deletions and drops the new `tests/v1/test_platform_run.py` with them. Nothing in verifiers imports `prime_runs` any more, so the dependency goes too; prime-rl depends on it directly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Why
The eval CLI hand-rolled its own platform client:
verifiers/v1/utils/platform.pywas 317 lines of resolve env → create → post samples → finalize over raw httpx, run once after the last rollout. prime#856 moves that lifecycle intoprime-runs, a leaf SDK (httpx+prime-traces) where a run is an object held for the length of the eval rather than three stateless calls at the end of it.What
platform.pyis now ~120 lines of wiring; the SDK owns transport, retries, backpressure and the terminal status:Gone:
push_traces,credentials,json_bytes,run_metrics, the batching loop, the payload ceiling, the in-memory buffer.trace_to_sampleandbuild_samplesmoved toprime_runs.projection(knowledge about a platform wire format, not about an eval framework).build_samplesstays importable from here because prime-rl's released monitor imports it across the repo boundary; nothing importstrace_to_sample.utils/platform.pyopen_run/finish_run/abort_run+PushState, the dashboard's read-through view of the runcli/eval/runner.pygather_rolloutscancels sibling rollouts on the first failure so nothing keeps uploading into a run that is being closedcli/eval/main.pyconfigs/cli/eval.pyRunConfigmints no id of its own:open_runassigns the prime-runs handle's id in every mode (the platform's evaluation id online, the SDK's local id otherwise), so the run dir, every trace on disk and the dashboard share one id. Linking uploads to the platform run is the SDK's job regardless (it re-keys every record it sends)cli/dashboard/eval.pyPushing traces (<url>)while it streams, whiteTraces pushed (<url>), red on failuretests/v1/conftest.pypush=Falseserve/client.pyEnvClient.close()gathers only the still-pending cancel senders: on Python 3.12+ a gather over finished tasks completes without yielding, so the loop spun at 100% CPU on Ctrl-C once the runner started waiting for rollouts to unwind before teardownNote
Replace manual HTTP trace pushing with
prime-runsSDK inplatform.pyprime-runsSDK for opening runs, logging episodes, and closing out runs.run_evalin runner.py, removing the legacy post-runpush_tracescall from main.py.gather_rolloutshelper to cancel and await all concurrent rollout tasks when any task fails or is cancelled before re-raising.cli.RunConfig.idto be assigned byopen_runduring execution instead of being eagerly generated as a UUID.cli.RunConfig.idproperty now raises if accessed beforeopen_runassigns an ID.Changes since #2415 opened
closingflag toverifiers.v1.cli.eval.runner.run_evalthat preventsabort_runfrom being called oncefinish_runhas been initiated [10fea3a]verifiers.v1.utils.platformto clarifyfinish_runsemantics and report actual run status [10fea3a]run_evalfunction in the evaluation runner to shieldfinish_runexecution from cancellation usingrun_shieldedwrappingasyncio.to_thread, removed theclosingboolean guard, and updated exception handling to unconditionally invokeabort_runfor any exception in the try block [9706c82]abort_runfunction to return early whenrun.statusis notpr.RunStatus.RUNNINGin addition to checking ifrun.finishedisTrue, and updated the docstring to document thatfinish()setsrun.statusas it starts [9706c82]Macroscope summarized e06c307.
Note
Medium Risk
Changes eval lifecycle, run identity, and platform upload timing across runner and config; failures are mostly best-effort, but code that reads
run.idbeforeopen_runwill now raise.Overview
Replaces the eval CLI’s hand-rolled httpx upload (create eval → batch samples → finalize after the last rollout) with
prime-runs, which owns the run for the whole eval.run_evalnow callsopen_runbefore rollouts,log_episodesas each episode finishes (and for resumed episodes), andfinish_run/abort_runon success or failure. The post-runpush_tracespath inmainis removed.gather_rolloutscancels sibling rollouts when one fails so uploads don’t continue into a closing run; close-out runs off the event loop behindrun_shielded.RunConfig.idis no longer a client UUID—it is assigned once byopen_runfrom the SDK handle (platform id online, local id when push is off).PushStateand the rich dashboard footer reflect streaming status, partial upload loss, and close-out errors.Adds
prime-runs>=0.1.1(and lock entries forprime-traces); E2E tests setpush=False.Reviewed by Cursor Bugbot for commit 9706c82. Bugbot is set up for automated code reviews on this repo. Configure here.