verifiers 0.2.2.dev94__py3-none-any.whl → 0.2.2.dev96__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ from verifiers.envs.experimental.utils.git_checkout_cache import (
15
15
  validate_git_checkout,
16
16
  )
17
17
  from verifiers.types import Messages, State, SystemMessage, TrajectoryStep
18
+ from verifiers.utils.path_utils import CACHE_DIR
18
19
 
19
20
  DEFAULT_RLM_REPO_URL = "github.com/PrimeIntellect-ai/rlm-harness.git"
20
21
  DEFAULT_RLM_REF = "main"
@@ -24,9 +25,7 @@ DEFAULT_RLM_MAX_DEPTH = 0
24
25
  DEFAULT_APPEND_TO_SYSTEM_PROMPT_PATH = "/task/append_to_system_prompt.txt"
25
26
  DEFAULT_RLM_CHECKOUT_PATH = "/tmp/rlm-checkout"
26
27
  DEFAULT_RLM_CHECKOUT_UPLOAD_NAME = "rlm_checkout"
27
- DEFAULT_RLM_LOCAL_CHECKOUT_CACHE_ROOT = (
28
- Path.home() / ".cache" / "verifiers" / "rlm-checkouts"
29
- )
28
+ DEFAULT_RLM_LOCAL_CHECKOUT_CACHE_ROOT = CACHE_DIR / "rlm-checkouts"
30
29
  _REQUIRED_CHECKOUT_FILES = ("install.sh", "pyproject.toml")
31
30
 
32
31
  COMPACTION_BOUNDARY_MARKER = "--- context compacted ---"
@@ -16,6 +16,7 @@ from verifiers.envs.experimental.utils.file_locks import (
16
16
  exclusive_path_lock,
17
17
  sibling_lock_path,
18
18
  )
19
+ from verifiers.utils.path_utils import CACHE_DIR
19
20
 
20
21
  _IN_USE_LOCK_SUFFIX = ".in-use.lock"
21
22
 
@@ -23,7 +24,7 @@ _IN_USE_LOCK_SUFFIX = ".in-use.lock"
23
24
  # resolved checkout's in-use lock file. See ``_acquire_in_use_lock``.
24
25
  _held_in_use_locks: dict[Path, IO] = {}
25
26
 
26
- DEFAULT_GIT_CHECKOUT_CACHE_ROOT = Path.home() / ".cache" / "verifiers" / "git-checkouts"
27
+ DEFAULT_GIT_CHECKOUT_CACHE_ROOT = CACHE_DIR / "git-checkouts"
27
28
  _FULL_COMMIT_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
28
29
 
29
30
  logger = logging.getLogger(__name__)
@@ -9,6 +9,18 @@ from verifiers.types import EvalConfig
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
+ def home_dir() -> Path:
13
+ """Best-effort home directory; fall back to the temp dir so import never fails."""
14
+ try:
15
+ return Path.home()
16
+ except RuntimeError:
17
+ return Path(tempfile.gettempdir())
18
+
19
+
20
+ CACHE_DIR = home_dir() / ".cache" / "verifiers"
21
+ """User-local cache root for verifiers-managed state."""
22
+
23
+
12
24
  def write_temp_file(content: str, suffix: str = ".txt") -> str:
13
25
  """Write content to a named temporary file and return its path.
14
26
 
@@ -7,7 +7,7 @@ import sys
7
7
  from pydantic_config import cli
8
8
 
9
9
  import verifiers.v1 as vf
10
- from verifiers.v1.cli.eval.resume import load_resume_config, split_resume
10
+ from verifiers.v1.cli.eval.resume import load_resume_config
11
11
  from verifiers.v1.cli.eval.runner import run_eval
12
12
  from verifiers.v1.cli.output import output_path, write_config
13
13
  from verifiers.v1.cli.resolve import (
@@ -17,6 +17,7 @@ from verifiers.v1.cli.resolve import (
17
17
  references_config_file,
18
18
  with_positional_taskset,
19
19
  )
20
+ from verifiers.v1.cli.resume import split_resume
20
21
  from verifiers.v1.configs.cli.eval import EvalConfig
21
22
  from verifiers.v1.utils.interrupt import install_interrupt
22
23
  from verifiers.v1.utils.logging import setup_logging
@@ -40,7 +41,7 @@ def main(argv: list[str] | None = None) -> None:
40
41
  narrow_config(EvalConfig, argv)
41
42
  ) # full option help, narrowed to the given ids
42
43
  return
43
- resume_dir, rest = split_resume(argv)
44
+ resume_dir, rest = split_resume(argv, "eval")
44
45
  # re-run a previous run's missing/errored rollouts, in place
45
46
  if resume_dir is not None:
46
47
  if rest:
@@ -11,7 +11,6 @@ changed since the interrupted run re-runs, and nothing depends on `data.idx`. Th
11
11
  legacy (v0) bridge still matches by row index (`key_of`).
12
12
  """
13
13
 
14
- import hashlib
15
14
  import json
16
15
  import tomllib
17
16
  from collections import Counter, defaultdict
@@ -22,6 +21,7 @@ from typing import TypeVar
22
21
  from pydantic_core import from_json
23
22
 
24
23
  from verifiers.v1.cli.output import CONFIG_FILE, TRACES_FILE, sniff_episode
24
+ from verifiers.v1.cli.resume import task_key
25
25
  from verifiers.v1.configs.cli.eval import EvalConfig
26
26
  from verifiers.v1.episode import Episode, WireEpisode
27
27
  from verifiers.v1.trace import WireTrace
@@ -29,43 +29,6 @@ from verifiers.v1.trace import WireTrace
29
29
  K = TypeVar("K", bound=Hashable)
30
30
 
31
31
 
32
- def task_key(data: Mapping) -> str:
33
- """Content identity of one task's wire data — an `exclude_none` dump, the shape
34
- saved rows already have on disk. `sort_keys` so field order can't split identity."""
35
- return hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
36
-
37
-
38
- def distribute(
39
- selected_keys: list[K], owed: dict[K, int], num_rollouts: int
40
- ) -> list[int]:
41
- """Spread each key's owed rollouts over its selection instances, in order —
42
- content-identical tasks are interchangeable, so any instance can absorb the
43
- debt (capped at `num_rollouts` each). Returns one count per selection."""
44
- remaining = dict(owed)
45
- counts: list[int] = []
46
- for key in selected_keys:
47
- take = min(num_rollouts, remaining.get(key, 0))
48
- if take:
49
- remaining[key] -= take
50
- counts.append(take)
51
- return counts
52
-
53
-
54
- def split_resume(argv: list[str]) -> tuple[Path | None, list[str]]:
55
- """Pull `--resume <dir>` / `--resume=<dir>` out of argv, returning (dir, the other args).
56
- The caller rejects any leftover args, since resume re-runs the saved config verbatim."""
57
- for i, arg in enumerate(argv):
58
- if arg == "--resume":
59
- if i + 1 >= len(argv):
60
- raise SystemExit(
61
- "--resume needs an output dir: uv run eval --resume <dir>"
62
- )
63
- return Path(argv[i + 1]), argv[:i] + argv[i + 2 :]
64
- if arg.startswith("--resume="):
65
- return Path(arg.split("=", 1)[1]), argv[:i] + argv[i + 1 :]
66
- return None, argv
67
-
68
-
69
32
  def load_resume_config(resume_dir: Path) -> EvalConfig:
70
33
  """Rebuild the run's `EvalConfig` from its saved `config.toml`, pointed back at its own
71
34
  output dir so the resumed rollouts append to the same `traces.jsonl`."""
@@ -14,6 +14,7 @@ from verifiers.v1.cli.output import (
14
14
  output_path,
15
15
  save_config,
16
16
  )
17
+ from verifiers.v1.cli.resume import distribute, task_key
17
18
  from verifiers.v1.clients import ModelContext
18
19
  from verifiers.v1.configs.cli.eval import EvalConfig
19
20
  from verifiers.v1.env import Env, RunSlot
@@ -48,14 +49,13 @@ async def run_eval(env: Env, config: EvalConfig) -> list[Episode]:
48
49
  finished: list[Episode] = []
49
50
  if config.resume is not None:
50
51
  keys = [
51
- resume.task_key(t.data.model_dump(mode="json", exclude_none=True))
52
- for t in tasks
52
+ task_key(t.data.model_dump(mode="json", exclude_none=True)) for t in tasks
53
53
  ]
54
54
  finished, owed = resume.load(out, keys, config.num_rollouts, env.complete)
55
55
  if not owed: # already complete - report it and exit successfully
56
56
  print(resume.nothing_to_resume_msg(out, len(tasks), config.num_rollouts))
57
57
  raise SystemExit(0)
58
- counts = resume.distribute(keys, owed, config.num_rollouts)
58
+ counts = distribute(keys, owed, config.num_rollouts)
59
59
  plan = [(task, n) for task, n in zip(tasks, counts) if n]
60
60
  logger.info(
61
61
  "resuming %s: %d task(s), %d rollout(s) owed",
@@ -179,7 +179,7 @@ async def run_eval_server(config: EvalConfig) -> list[Episode]:
179
179
  client = EnvClient(address=address)
180
180
  await client.wait_for_server_startup(timeout=600)
181
181
  # A v1 run dispatches — and resumes — tasks by content: the client owns them,
182
- # and `resume.task_key` is their identity. Only the legacy bridge is addressed
182
+ # and `task_key` is their identity. Only the legacy bridge is addressed
183
183
  # by dataset row (its dataset lives server-side, reported via `info`), and
184
184
  # only a legacy env group-scores; a v1 env scores siblings in its own rollout.
185
185
  if legacy:
@@ -209,14 +209,14 @@ async def run_eval_server(config: EvalConfig) -> list[Episode]:
209
209
  whole_task=group_scored,
210
210
  key_of=lambda data: data.get("idx"),
211
211
  )
212
- counts = resume.distribute(idxs, owed, config.num_rollouts)
212
+ counts = distribute(idxs, owed, config.num_rollouts)
213
213
  else:
214
214
  keys = [
215
- resume.task_key(t.data.model_dump(mode="json", exclude_none=True))
215
+ task_key(t.data.model_dump(mode="json", exclude_none=True))
216
216
  for t in tasks
217
217
  ]
218
218
  finished, owed = resume.load(out, keys, config.num_rollouts)
219
- counts = resume.distribute(keys, owed, config.num_rollouts)
219
+ counts = distribute(keys, owed, config.num_rollouts)
220
220
  if not owed: # already complete - report it and exit successfully
221
221
  print(resume.nothing_to_resume_msg(out, len(plan), config.num_rollouts))
222
222
  raise SystemExit(0)
@@ -0,0 +1,42 @@
1
+ """Resume primitives shared by eval-like CLIs."""
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Hashable, Mapping
6
+ from pathlib import Path
7
+ from typing import TypeVar
8
+
9
+ K = TypeVar("K", bound=Hashable)
10
+
11
+
12
+ def task_key(data: Mapping) -> str:
13
+ """Content identity for task wire data, independent of field order."""
14
+ return hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
15
+
16
+
17
+ def distribute(
18
+ selected_keys: list[K], owed: dict[K, int], num_results: int
19
+ ) -> list[int]:
20
+ """Spread each key's owed results over its selected instances, in order."""
21
+ remaining = dict(owed)
22
+ counts: list[int] = []
23
+ for key in selected_keys:
24
+ take = min(num_results, remaining.get(key, 0))
25
+ if take:
26
+ remaining[key] -= take
27
+ counts.append(take)
28
+ return counts
29
+
30
+
31
+ def split_resume(argv: list[str], command: str) -> tuple[Path | None, list[str]]:
32
+ """Pull ``--resume <dir>`` from argv, returning the dir and other arguments."""
33
+ for i, arg in enumerate(argv):
34
+ if arg == "--resume":
35
+ if i + 1 >= len(argv):
36
+ raise SystemExit(
37
+ f"--resume needs an output dir: uv run {command} --resume <dir>"
38
+ )
39
+ return Path(argv[i + 1]), argv[:i] + argv[i + 2 :]
40
+ if arg.startswith("--resume="):
41
+ return Path(arg.split("=", 1)[1]), argv[:i] + argv[i + 1 :]
42
+ return None, argv
@@ -2,16 +2,23 @@
2
2
 
3
3
  import asyncio
4
4
  import contextlib
5
+ import json
5
6
  import logging
6
7
  import sys
7
8
  import time
9
+ import tomllib
10
+ from collections import Counter, defaultdict
11
+ from collections.abc import Mapping, Sequence
12
+ from pathlib import Path
8
13
  from typing import Any
9
14
  from uuid import uuid4
10
15
 
11
16
  from pydantic_config import cli
17
+ from pydantic_core import from_json
12
18
 
13
19
  import verifiers.v1 as vf
14
20
  from verifiers.v1.cli.dashboard import TaskProgress, validate_dashboard
21
+ from verifiers.v1.cli.output import CONFIG_FILE, write_config
15
22
  from verifiers.v1.cli.resolve import (
16
23
  extract_id,
17
24
  narrow_taskset_config,
@@ -19,11 +26,13 @@ from verifiers.v1.cli.resolve import (
19
26
  references_config_file,
20
27
  with_positional_taskset,
21
28
  )
29
+ from verifiers.v1.cli.resume import distribute, split_resume, task_key
22
30
  from verifiers.v1.configs.cli.validate import ValidateConfig
23
31
  from verifiers.v1.runtimes import make_runtime
24
32
  from verifiers.v1.state import state_cls
25
33
  from verifiers.v1.task import Task
26
34
  from verifiers.v1.trace import Trace, TraceTask
35
+ from verifiers.v1.utils.aio import run_shielded
27
36
  from verifiers.v1.utils.compile import resolve_runtime_config
28
37
  from verifiers.v1.utils.decorators import invoke
29
38
  from verifiers.v1.utils.interrupt import install_interrupt
@@ -31,10 +40,19 @@ from verifiers.v1.utils.logging import setup_logging
31
40
 
32
41
  logger = logging.getLogger(__name__)
33
42
 
43
+ RESULTS_FILE = "results.jsonl"
44
+ SUMMARY_FILE = "summary.json"
45
+ LOG_FILE = "validate.log"
46
+ FINAL_REASONS = frozenset({"valid", "invalid"})
47
+ REASONS = ("valid", "invalid", "error", "timeout")
48
+
49
+ ResultRow = dict[str, Any]
50
+
34
51
  USAGE = (
35
52
  "usage: uv run validate [<taskset-id>] [--only-setup | --only-gold] "
36
- "[--runtime.type subprocess] [options] [@ file.toml]\n"
37
- " runs the gold and setup-only checks per task (no model)"
53
+ "[-o <output-dir>] [--runtime.type subprocess] [options] [@ file.toml]\n"
54
+ " uv run validate --resume <output-dir>\n"
55
+ " runs persisted gold and setup-only checks per task (no model)"
38
56
  )
39
57
 
40
58
 
@@ -45,7 +63,149 @@ def _narrow(argv: list[str]) -> type[ValidateConfig]:
45
63
  return narrow_taskset_config(ValidateConfig, extract_id(argv, "taskset"))
46
64
 
47
65
 
48
- ResultRow = dict[str, Any]
66
+ def validation_mode(config: ValidateConfig) -> str:
67
+ if config.only_gold:
68
+ return "gold"
69
+ if config.only_setup:
70
+ return "setup"
71
+ return "all"
72
+
73
+
74
+ def output_path(config: ValidateConfig) -> Path:
75
+ if config.output_dir is not None:
76
+ return config.output_dir
77
+ return Path("outputs") / f"{config.name}--validate" / config.uuid
78
+
79
+
80
+ def _write_rows(path: Path, rows: Sequence[ResultRow]) -> None:
81
+ tmp = path.with_suffix(f"{path.suffix}.tmp")
82
+ with tmp.open("w", encoding="utf-8") as f:
83
+ for row in rows:
84
+ f.write(json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n")
85
+ tmp.replace(path)
86
+
87
+
88
+ def append_result(results_dir: Path, row: ResultRow) -> None:
89
+ data = json.dumps(row, sort_keys=True, separators=(",", ":")).encode()
90
+ with (results_dir / RESULTS_FILE).open("ab") as f:
91
+ f.write(data + b"\n")
92
+
93
+
94
+ def _is_final(row: object, key: str, mode: str) -> bool:
95
+ if not isinstance(row, dict):
96
+ return False
97
+ reason = row.get("reason")
98
+ return (
99
+ row.get("task_key") == key
100
+ and row.get("mode") == mode
101
+ and reason in FINAL_REASONS
102
+ and row.get("valid") is (reason == "valid")
103
+ )
104
+
105
+
106
+ def load_results(
107
+ results_dir: Path, selected_keys: list[str], mode: str
108
+ ) -> tuple[list[ResultRow], dict[str, int]]:
109
+ """Keep final rows by eval task-content key; return counts owed per key."""
110
+ path = results_dir / RESULTS_FILE
111
+ targets = Counter(selected_keys)
112
+ good: dict[str, list[ResultRow]] = defaultdict(list)
113
+ if path.exists():
114
+ with path.open("rb") as f:
115
+ for line in f:
116
+ if not line.strip():
117
+ continue
118
+ try:
119
+ row = from_json(line)
120
+ except ValueError:
121
+ try:
122
+ row = json.loads(line)
123
+ except (json.JSONDecodeError, UnicodeDecodeError):
124
+ continue
125
+ if not isinstance(row, dict):
126
+ continue
127
+ key = row.get("task_key")
128
+ if (
129
+ isinstance(key, str)
130
+ and key in targets
131
+ and len(good[key]) < targets[key]
132
+ and _is_final(row, key, mode)
133
+ ):
134
+ good[key].append(row)
135
+
136
+ owed = {
137
+ key: target - len(good.get(key, []))
138
+ for key, target in targets.items()
139
+ if len(good.get(key, [])) < target
140
+ }
141
+ # Eval spreads debt over content-identical selected tasks in order. Assign the
142
+ # interchangeable kept rows to the remaining positions so reporting positions
143
+ # stay unique even when duplicate task content exists.
144
+ counts = distribute(selected_keys, owed, 1)
145
+ rows = []
146
+ used = Counter()
147
+ for position, (key, count) in enumerate(zip(selected_keys, counts)):
148
+ if count:
149
+ continue
150
+ row = good[key][used[key]]
151
+ used[key] += 1
152
+ rows.append({**row, "task_position": position})
153
+ _write_rows(path, rows)
154
+ return rows, owed
155
+
156
+
157
+ def summarize(rows: Sequence[ResultRow], total: int, mode: str) -> dict[str, Any]:
158
+ counts = Counter(row.get("reason") for row in rows)
159
+ missing = max(0, total - len(rows))
160
+ outcomes = {reason: counts[reason] for reason in REASONS}
161
+ outcomes["missing"] = missing
162
+ terminal = outcomes["valid"] + outcomes["invalid"]
163
+ summary: dict[str, Any] = {
164
+ "mode": mode,
165
+ "total": total,
166
+ "recorded": len(rows),
167
+ "terminal": terminal,
168
+ "owed": missing + outcomes["error"] + outcomes["timeout"],
169
+ "outcomes": outcomes,
170
+ "valid_rate": round(outcomes["valid"] / total, 6) if total else None,
171
+ }
172
+ if mode == "all":
173
+ checks: dict[str, dict[str, int]] = {}
174
+ for check in ("gold", "setup"):
175
+ check_counts = Counter(
176
+ row.get(check, {}).get("reason")
177
+ for row in rows
178
+ if isinstance(row.get(check), dict)
179
+ )
180
+ checks[check] = {reason: check_counts[reason] for reason in REASONS}
181
+ checks[check]["missing"] = missing
182
+ summary["checks"] = checks
183
+ return summary
184
+
185
+
186
+ def write_summary(results_dir: Path, summary: Mapping[str, Any]) -> None:
187
+ path = results_dir / SUMMARY_FILE
188
+ tmp = path.with_suffix(f"{path.suffix}.tmp")
189
+ tmp.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
190
+ tmp.replace(path)
191
+
192
+
193
+ def save_run(config: ValidateConfig, results_dir: Path, total: int) -> None:
194
+ write_config(config, results_dir)
195
+ (results_dir / RESULTS_FILE).write_text("")
196
+ write_summary(results_dir, summarize([], total, validation_mode(config)))
197
+
198
+
199
+ def load_resume_config(resume_dir: Path) -> ValidateConfig:
200
+ path = resume_dir / CONFIG_FILE
201
+ if not path.exists():
202
+ raise SystemExit(
203
+ f"--resume: no config.toml in {resume_dir} - not a validate output dir"
204
+ )
205
+ config = ValidateConfig.model_validate(tomllib.loads(path.read_text()))
206
+ config.resume = resume_dir
207
+ config.output_dir = resume_dir
208
+ return config
49
209
 
50
210
 
51
211
  def _classify(valid: bool, exc: BaseException | None) -> str:
@@ -217,27 +377,64 @@ async def run_validate(config: ValidateConfig) -> list[dict]:
217
377
  raise SystemExit(
218
378
  "taskset needs a container runtime to validate - pass --runtime.type docker (or prime)"
219
379
  )
220
- checks = (
221
- "gold" if config.only_gold else "setup" if config.only_setup else "gold+setup"
222
- )
380
+ mode = validation_mode(config)
381
+ checks = "gold+setup" if mode == "all" else mode
382
+ out = output_path(config)
383
+ selected_keys = [
384
+ task_key(task.data.model_dump(mode="json", exclude_none=True)) for task in tasks
385
+ ]
386
+ if config.resume is None:
387
+ save_run(config, out, len(tasks))
388
+ rows: list[ResultRow] = []
389
+ counts = [1] * len(tasks)
390
+ else:
391
+ rows, owed = load_results(out, selected_keys, mode)
392
+ counts = distribute(selected_keys, owed, 1)
393
+ write_summary(out, summarize(rows, len(tasks), mode))
394
+ plan = [
395
+ (position, task, key)
396
+ for position, (task, key, count) in enumerate(zip(tasks, selected_keys, counts))
397
+ if count
398
+ ]
223
399
  logger.info(
224
- "validating %d task(s) from %s on the %s runtime (%s)",
400
+ "%s %d/%d task(s) from %s on the %s runtime (%s)",
401
+ "resuming" if config.resume is not None else "validating",
402
+ len(plan),
225
403
  len(tasks),
226
404
  config.name,
227
405
  config.runtime.type,
228
406
  checks,
229
407
  )
408
+ logger.info("results: %s", out)
230
409
 
231
410
  sem = asyncio.Semaphore(config.max_concurrent) if config.max_concurrent else None
232
411
  states = [TaskProgress(idx=t.data.idx, name=t.data.name) for t in tasks]
233
- state_by_idx = {s.idx: s for s in states}
412
+ for row in rows:
413
+ state = states[row["task_position"]]
414
+ state.state = row["reason"]
415
+
416
+ write_lock = asyncio.Lock()
234
417
 
235
- async def _one(task) -> dict:
236
- st = state_by_idx[task.data.idx]
418
+ async def _one(position: int, task: Task, key: str) -> ResultRow:
419
+ st = states[position]
237
420
  async with sem or contextlib.nullcontext():
238
421
  st.start = time.time()
239
422
  st.state = "running"
240
423
  row = await _validate_task(task, config)
424
+ row["task_position"] = position
425
+ row["task_key"] = key
426
+
427
+ async def persist() -> None:
428
+ async with write_lock:
429
+ await asyncio.to_thread(append_result, out, row)
430
+ rows.append(row)
431
+ await asyncio.to_thread(
432
+ write_summary,
433
+ out,
434
+ summarize(rows, len(tasks), mode),
435
+ )
436
+
437
+ await run_shielded(persist())
241
438
  st.end, st.state = time.time(), row["reason"]
242
439
  if not config.rich: # the dashboard shows this live; otherwise log each task
243
440
  detail = f" - {row['error']}" if row["error"] else ""
@@ -257,7 +454,17 @@ async def run_validate(config: ValidateConfig) -> list[dict]:
257
454
  else contextlib.nullcontext()
258
455
  )
259
456
  async with display:
260
- return await asyncio.gather(*(_one(t) for t in tasks))
457
+ if not plan:
458
+ logger.info(
459
+ "nothing to resume: all %d task(s) are valid or invalid", len(tasks)
460
+ )
461
+ return rows
462
+ await asyncio.gather(
463
+ *(_one(position, task, key) for position, task, key in plan)
464
+ )
465
+ rows.sort(key=lambda row: row["task_position"])
466
+ write_summary(out, summarize(rows, len(tasks), mode))
467
+ return rows
261
468
 
262
469
 
263
470
  def main(argv: list[str] | None = None) -> None:
@@ -271,27 +478,46 @@ def main(argv: list[str] | None = None) -> None:
271
478
  with plugin_errors():
272
479
  cli(_narrow(argv)) # full option help, narrowed to the given taskset
273
480
  return
274
- if not extract_id(argv, "taskset") and not references_config_file(argv):
275
- raise SystemExit(
276
- USAGE
277
- ) # need a taskset (positional / --taskset.id) or a @ file.toml
278
-
279
- with plugin_errors():
280
- config_type = _narrow(argv)
281
- sys.argv = [
282
- sys.argv[0],
283
- *argv,
284
- ] # let prime-pydantic-config render help/errors
285
- config = cli(config_type)
286
- # Nothing is persisted, so logs are the whole output. Under `--rich` the dashboard owns the
287
- # screen, so keep logs off the console (else stray records print over the UI).
288
- setup_logging("DEBUG" if config.verbose else "INFO", console=not config.rich)
481
+ resume_dir, rest = split_resume(argv, "validate")
482
+ if resume_dir is not None:
483
+ if rest:
484
+ raise SystemExit(
485
+ f"{USAGE}\n--resume replays the saved config and takes no other arguments"
486
+ )
487
+ with plugin_errors():
488
+ config = load_resume_config(resume_dir)
489
+ else:
490
+ if not extract_id(argv, "taskset") and not references_config_file(argv):
491
+ raise SystemExit(
492
+ USAGE
493
+ ) # need a taskset (positional / --taskset.id) or a @ file.toml
494
+
495
+ with plugin_errors():
496
+ config_type = _narrow(argv)
497
+ sys.argv = [
498
+ sys.argv[0],
499
+ *argv,
500
+ ] # let prime-pydantic-config render help/errors
501
+ config = cli(config_type)
502
+ out = output_path(config)
503
+ setup_logging(
504
+ "DEBUG" if config.verbose else "INFO",
505
+ log_file=str(out / LOG_FILE),
506
+ console=not config.rich,
507
+ )
289
508
  if config.rich:
290
509
  logging.lastResort = None # drop stdlib records that bypass loguru
291
510
  # Graceful shutdown: first Ctrl-C/SIGTERM unwinds each task's teardown `finally`
292
511
  # (containers/sandboxes); a second is swallowed so it can't orphan them mid-cleanup.
293
512
  install_interrupt()
294
- asyncio.run(run_validate(config))
513
+ try:
514
+ asyncio.run(run_validate(config))
515
+ except KeyboardInterrupt:
516
+ print(f"interrupted; partial results: {out}", file=sys.stderr)
517
+ raise SystemExit(130)
518
+ summary = json.loads((out / SUMMARY_FILE).read_text())
519
+ print(f"results: {out}")
520
+ print(json.dumps(summary, indent=2, sort_keys=True))
295
521
 
296
522
 
297
523
  if __name__ == "__main__":
@@ -1,5 +1,8 @@
1
1
  """Configuration for model-free task validation."""
2
2
 
3
+ from pathlib import Path
4
+ from uuid import uuid4
5
+
3
6
  from pydantic import AliasChoices, Field, SerializeAsAny, model_validator
4
7
  from pydantic_config import BaseConfig
5
8
 
@@ -16,6 +19,9 @@ class CheckTimeoutConfig(BaseConfig):
16
19
 
17
20
 
18
21
  class ValidateConfig(BaseConfig):
22
+ uuid: str = Field(default_factory=lambda: str(uuid4()), exclude=True)
23
+ """Auto-generated run id — the default output directory leaf. Excluded from the
24
+ saved config so re-running it starts a fresh run."""
19
25
  taskset: SerializeAsAny[TasksetConfig] = TasksetConfig()
20
26
  runtime: RuntimeConfig = DockerConfig()
21
27
  """Where each task's validation hooks run."""
@@ -40,6 +46,14 @@ class ValidateConfig(BaseConfig):
40
46
  """Log at debug level instead of the default info."""
41
47
  rich: bool = True
42
48
  """Show a live dashboard (one row per task) instead of per-task log lines."""
49
+ output_dir: Path | None = Field(
50
+ None, validation_alias=AliasChoices("output_dir", "o")
51
+ )
52
+ """Where to write config.toml, results.jsonl, summary.json, and validate.log. None
53
+ creates a fresh run under outputs/<taskset>--validate/<uuid>."""
54
+ resume: Path | None = Field(None, exclude=True)
55
+ """Set by --resume: re-run missing, errored, and timed-out tasks in this directory.
56
+ The saved config is replayed verbatim, so resume takes no other arguments."""
43
57
 
44
58
  @property
45
59
  def name(self) -> str:
@@ -13,7 +13,7 @@ from verifiers.v1.runtimes.limiters import creation_limiter
13
13
  from verifiers.v1.utils.aio import run_shielded
14
14
 
15
15
  # The prime_tunnel service caps tunnel starts at 512/min per API token — a property of the
16
- # tunnel service, shared by every process on the host that opens one. One host-global
16
+ # tunnel service, shared by every process for the user that opens one. One user-global
17
17
  # limiter, not a per-runtime config knob.
18
18
  _TUNNELS_PER_MIN = 512
19
19
  TUNNEL_LIMITER = creation_limiter(_TUNNELS_PER_MIN / 60, "prime-tunnel")
@@ -30,7 +30,7 @@ class PrimeTunnel(Tunnel[PrimeTunnelConfig]):
30
30
  @contextlib.asynccontextmanager
31
31
  async def expose(self, port: int) -> AsyncIterator[str]:
32
32
  """Bridge the host `port` to a public URL via prime_tunnel (frpc). Tunnel creation
33
- is network-bound and globally rate-capped (512/min, host-wide via the shared
33
+ is network-bound and globally rate-capped (512/min, user-wide via the shared
34
34
  `TUNNEL_LIMITER`), so transient failures are retried; a terminal one raises
35
35
  `TunnelError`. The tunnel is torn down on exit."""
36
36
  from prime_tunnel import Tunnel as TunnelClient
@@ -1,41 +1,45 @@
1
- """Host-global creation-rate limiters for the remote runtimes.
1
+ """User-global creation-rate limiters for the remote runtimes.
2
2
 
3
- A leaky bucket backed by a lock file, so a provider's per-account creation rate (Modal
4
- sandboxes, Prime tunnels) is enforced across EVERY process on the host — the single-process
5
- eval and all the elastically-spawned env-server worker processes alike — not just within one
6
- process. So the configured rate is the actual account-wide rate (assuming one env-server
7
- host). Keyed by name: one bucket file per name, shared by every process (and run) on the host.
3
+ A leaky bucket backed by a lock file under the user cache (``~/.cache/verifiers``, falling
4
+ back to the temp dir when no home is resolvable), so a provider's per-account creation rate
5
+ (Modal sandboxes, Prime tunnels) is enforced across EVERY process for the user —
6
+ the single-process eval and all the elastically-spawned env-server worker processes alike — not
7
+ just within one process. Keyed by name: one bucket file per name, shared by every process (and
8
+ run) for the user.
8
9
  """
9
10
 
10
11
  import asyncio
11
12
  import fcntl
12
13
  import os
13
- import tempfile
14
14
  import time
15
15
  from typing import Self
16
16
 
17
- _LIMITER_DIR = os.path.join(tempfile.gettempdir(), "vf-rate-limiters")
17
+ from verifiers.utils.path_utils import CACHE_DIR
18
+
19
+ LIMITER_DIR = CACHE_DIR / "limiter"
18
20
 
19
21
 
20
22
  class CreationLimiter:
21
23
  """An async leaky bucket shared across processes via a lock file: each `async with`
22
24
  reserves the next `1/per_sec`-spaced slot (advancing the on-disk cursor under an exclusive
23
- flock) and sleeps until it, so the aggregate creation rate across all host processes stays
24
- at `per_sec`. The reservation runs off the event loop; the wait does not hold the lock."""
25
+ flock) and sleeps until it, so the aggregate creation rate across all of the user's
26
+ processes stays at `per_sec`. The reservation runs off the event loop; the wait does not
27
+ hold the lock."""
25
28
 
26
29
  def __init__(self, name: str, per_sec: float) -> None:
27
30
  self._interval = 1 / per_sec
28
- self._path = os.path.join(_LIMITER_DIR, f"{name}.bucket")
31
+ self._path = LIMITER_DIR / f"{name}.bucket"
29
32
 
30
33
  def _reserve(self) -> float:
31
- os.makedirs(_LIMITER_DIR, exist_ok=True)
32
- # monotonic is host-wide on Linux, so the cursor is comparable across processes.
34
+ os.makedirs(LIMITER_DIR, exist_ok=True)
35
+ # Wall clock persists across reboots (monotonic does not), so a cursor left in
36
+ # ~/.cache from a previous boot cannot turn into a huge stale wait.
33
37
  with open(self._path, "a+") as f:
34
38
  fcntl.flock(f.fileno(), fcntl.LOCK_EX)
35
39
  try:
36
40
  f.seek(0)
37
41
  data = f.read().strip()
38
- now = time.monotonic()
42
+ now = time.time()
39
43
  slot = max(now, float(data) if data else 0.0)
40
44
  f.seek(0)
41
45
  f.truncate()
@@ -59,7 +63,7 @@ _creation_limiters: dict[str, CreationLimiter] = {}
59
63
 
60
64
 
61
65
  def creation_limiter(per_sec: float | None, name: str) -> CreationLimiter | None:
62
- """A host-global limiter pacing `name`'s creation to `per_sec`/s (None/<= 0 disables).
66
+ """A user-global limiter pacing `name`'s creation to `per_sec`/s (None/<= 0 disables).
63
67
 
64
68
  All callers (and processes) sharing a `name` share one bucket, so use one rate per name."""
65
69
  if not per_sec or per_sec <= 0:
@@ -53,7 +53,7 @@ class ModalConfig(BaseConfig):
53
53
  """Disk in GB. Modal sandboxes have no disk knob, so this is accepted (so a task can
54
54
  declare it without a warning) but not enforced."""
55
55
  creates_per_sec: float | None = 40.0
56
- """Pace sandbox creation to this many per second, enforced host-wide across every
56
+ """Pace sandbox creation to this many per second, enforced user-wide across every
57
57
  env-server worker process (None/<= 0 disables it)."""
58
58
 
59
59
 
@@ -66,7 +66,7 @@ class PrimeConfig(NetworkPolicyConfig):
66
66
  idle_timeout: float | None = 3600
67
67
  """Seconds of inactivity before the sandbox self-deletes (None disables)."""
68
68
  creates_per_min: int | None = None
69
- """Pace sandbox creation to this many per minute, enforced host-wide across every
69
+ """Pace sandbox creation to this many per minute, enforced user-wide across every
70
70
  env-server worker process (None/<= 0 disables it). (Tunnel creation is limited separately
71
71
  and globally — see interception.tunnel.prime.TUNNEL_LIMITER.)"""
72
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verifiers
3
- Version: 0.2.2.dev94
3
+ Version: 0.2.2.dev96
4
4
  Summary: Verifiers: Environments for LLM Reinforcement Learning
5
5
  Project-URL: Homepage, https://github.com/primeintellect-ai/verifiers
6
6
  Project-URL: Documentation, https://github.com/primeintellect-ai/verifiers
@@ -52,7 +52,7 @@ verifiers/envs/experimental/composable/harnesses/__init__.py,sha256=cL29z-h8RY8v
52
52
  verifiers/envs/experimental/composable/harnesses/mini_swe_agent.py,sha256=-oJ4Nz2DZOzMEbVtxgcGvnzAZYXt-pdz3B-6hNsoU2A,8882
53
53
  verifiers/envs/experimental/composable/harnesses/opencode.py,sha256=u7raPlUrAL2C_nX4WTAt-1dWkEVR5iLeQhvjXJB3Pkk,10338
54
54
  verifiers/envs/experimental/composable/harnesses/prompt.txt,sha256=dpQFIXxhad0EFZ0SeYuRvt5upTrhfb-d4BxfRL-i0OY,1633
55
- verifiers/envs/experimental/composable/harnesses/rlm.py,sha256=aIjYcvjhp4C7RWcJQ2Hb-dhYU5lcZ8V-e4YYZXZnh24,13081
55
+ verifiers/envs/experimental/composable/harnesses/rlm.py,sha256=keRKbNK1zsCqxewlbp-8cCQHgC2L90GyPJxNOcp7EY8,13095
56
56
  verifiers/envs/experimental/composable/tasksets/__init__.py,sha256=5VKUn5W11Ekx6Issyg0l5rs1n0kNapJYPAIpVnaNBsg,1995
57
57
  verifiers/envs/experimental/composable/tasksets/cp/__init__.py,sha256=Aj5uBcEbzkgmBz5ZX7Wl044fEJcFxS0q0CZqs-SU75s,92
58
58
  verifiers/envs/experimental/composable/tasksets/cp/cp_task.py,sha256=aODpFS85pVqArflN0IZ22dA-u_oGH_cB4X-RjvFnuuo,7882
@@ -92,7 +92,7 @@ verifiers/envs/experimental/harbor_env/env.py,sha256=ObexUEWqPRDnjbnDmDi4zo4frWK
92
92
  verifiers/envs/experimental/harbor_env/mcp.py,sha256=_XrwnqnkIHUp_GA6sMua5cMTLm0boD-EJulQ8KqVTxw,12343
93
93
  verifiers/envs/experimental/utils/__init__.py,sha256=MLspkUZeGYH3TNv8FXIq2knBEnUAS8bvkK4pMXeKs7M,575
94
94
  verifiers/envs/experimental/utils/file_locks.py,sha256=S6e4Z8lFSepp-Bbvl3xA4uT9WbcuyI_6ZcV8k0q9DXc,1424
95
- verifiers/envs/experimental/utils/git_checkout_cache.py,sha256=wt0EqNQGZMVVWr0ERY67dHunpoKfLSaXp09FPj0bhKI,12117
95
+ verifiers/envs/experimental/utils/git_checkout_cache.py,sha256=iEZIJIbYV-6RBleHoPZPHQ8zDIPiZBGcH3RSdYRxjf4,12139
96
96
  verifiers/envs/integrations/README.md,sha256=trk8lpwTq7uCQBIbid8aNa4O0Q5YnQWmx-X7xQsZQHM,7226
97
97
  verifiers/envs/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  verifiers/envs/integrations/openenv_env.py,sha256=MrthKsss3QmuphyW4cHLH14yXBGilRiXtZs4Vn5rzIw,35745
@@ -156,7 +156,7 @@ verifiers/utils/interception_utils.py,sha256=JFppjEGmx2bWcX8VOMjd786DcStkt0xhBTY
156
156
  verifiers/utils/logging_utils.py,sha256=ORhY77WdhjhExTC8ckXBMubVt-bLwzETYKzZL-zuH9U,8529
157
157
  verifiers/utils/message_utils.py,sha256=c1orN5CVFSK4Hk2ai__LhBV4xboZcSyyWtpltAt15w4,17884
158
158
  verifiers/utils/metric_utils.py,sha256=sdjoixAwjcRBfRHPKH33RSrPB8HVpLXlGa9CzvB8Z-o,6825
159
- verifiers/utils/path_utils.py,sha256=DU1xOkGUWuQZ6d4LoDI8Thh5blmPCSa3OQXQP67YKLg,6077
159
+ verifiers/utils/path_utils.py,sha256=s0Nnx2x2sXqDQ5Ml7vVwteLHnXa8ZsFAgVevF2NON28,6401
160
160
  verifiers/utils/pricing_utils.py,sha256=V5FaqVUOL02z7JDy4NtPe98UZ9OC-5Sd0oMHSZlwwvg,5156
161
161
  verifiers/utils/process_utils.py,sha256=tFizSYGIUFieg4eGP3dYM2EonB2qmlCzl5EZX8qjmak,2873
162
162
  verifiers/utils/response_utils.py,sha256=E3CrgZ9N_6pCQGxQEI78xAHH0LUGK6utuI01g5Q1NEk,5579
@@ -192,16 +192,17 @@ verifiers/v1/cli/init.py,sha256=Qj96Kwzfhaejhrc0yYqHNRl3ncoID028RGlHvS1C98Q,8508
192
192
  verifiers/v1/cli/output.py,sha256=JiNjMO2kwM4p3zMPg3k4spuJbbE7-jOAtYBttPap7qI,6045
193
193
  verifiers/v1/cli/replay.py,sha256=DAh9kB5PB-ppZLGhxdJwrCPch_Yky5HKhplwGFhKEz0,10164
194
194
  verifiers/v1/cli/resolve.py,sha256=OsNw4C3r9xOVFa-mminJFcex_bAsJ6EQ8K7A_H8u2kQ,4370
195
- verifiers/v1/cli/validate.py,sha256=7ax6FqIzNBSYjd3JXK4v7Vbq34Rozyh1OvGpYnGqlSg,10266
195
+ verifiers/v1/cli/resume.py,sha256=eW9EJU-l4pXufyuNA5nLXMdeRPcbdU6_YMDnYr7_R5I,1422
196
+ verifiers/v1/cli/validate.py,sha256=f-6gJPoSvs5psTKirZQ3VjP4NvjJdFxY2EFnV0gkUfk,18206
196
197
  verifiers/v1/cli/dashboard/__init__.py,sha256=v-baMxQuWxOCsbU7-p_jj2Q9BUnTN-TWi1q2hK6rU2s,198
197
198
  verifiers/v1/cli/dashboard/base.py,sha256=kUP93zJSIVLptSbnWX6MOy8kGg63fpeAzPqakCpPyDc,3547
198
199
  verifiers/v1/cli/dashboard/eval.py,sha256=4j2YCPyzNHDT7d8MwqYR3C906lzwDVAd3FxgFYFgsw0,33588
199
200
  verifiers/v1/cli/dashboard/replay.py,sha256=CvRVaf0dUum5v0IzPQbFPPikBQmTrkMeaunGfVMTWHc,2755
200
201
  verifiers/v1/cli/dashboard/validate.py,sha256=rLsQ_31DjTIZpC_VO7zSG41ncUEBxqCx8mL9nTnGiMc,3650
201
202
  verifiers/v1/cli/eval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
202
- verifiers/v1/cli/eval/main.py,sha256=xiMMIIbsy77SEOryHXOQyswS9WyrPKTyBwW65W9WTjI,5511
203
- verifiers/v1/cli/eval/resume.py,sha256=QwXuLPs2lN-aMZk1CyaT12egRPzeI6vfY39DAd0nYLU,7171
204
- verifiers/v1/cli/eval/runner.py,sha256=iR_Bapy5r0IC43GpzlfKIO9RAzFmKcY6EMcwYbEJupY,12181
203
+ verifiers/v1/cli/eval/main.py,sha256=12eDekY8IsJkwiodHepf8pxqYulO2uSp8_Z29xIPXA4,5554
204
+ verifiers/v1/cli/eval/resume.py,sha256=tFMAUHSlnNgfEbh14vc2uGI6jzWi7pVx0NNK61jmpiQ,5644
205
+ verifiers/v1/cli/eval/runner.py,sha256=ufWTbONCAuIU-c0-DluYU_EnbRoEivYVsJ-kHTuHjf4,12184
205
206
  verifiers/v1/clients/__init__.py,sha256=Ysig0tE_0E4Jsfgfes1XHN-fK1s_RCXdqZD6E7lK4PU,507
206
207
  verifiers/v1/clients/base.py,sha256=hhtTumKC7lgI6cj7Hks91DCjqNTwAh40DD_j8CWGd6c,1560
207
208
  verifiers/v1/clients/client.py,sha256=L4axOl-dF9_MSYagB_xAO5qbeE53jNZJnB39VLdVBwo,3156
@@ -224,7 +225,7 @@ verifiers/v1/configs/cli/env.py,sha256=Qwba1wD2Upbh2W09YbSpktueCJZaWV_DymHRaPgl_
224
225
  verifiers/v1/configs/cli/eval.py,sha256=HqIBtt8DuZEHZ07KgtE1jiqZy8OsDMfTahTuBAvslcA,6863
225
226
  verifiers/v1/configs/cli/init.py,sha256=tMaTDF_lU1Af3JmQpksk2mwe8CAfpnHsquXhwepZhYo,1019
226
227
  verifiers/v1/configs/cli/replay.py,sha256=xvMDZzIkvjgYl-a3ragjXI65DsD6FYl6gEFM5qpdIuA,2913
227
- verifiers/v1/configs/cli/validate.py,sha256=UdgC6bQ2RYx3t8n9HjsImAHV_ifoOxYOSlbtdZzOnZY,2298
228
+ verifiers/v1/configs/cli/validate.py,sha256=Yy3bJdCHdnLblaG-l84xAFHg9YgHfg2i-gVwH1hdOsg,3048
228
229
  verifiers/v1/dialects/__init__.py,sha256=p72C7VYrFQzVgTcfmzpHGSwXlvThfc4pvjBxsqw7kHE,786
229
230
  verifiers/v1/dialects/anthropic.py,sha256=nj9J7OOhexmPHa-sWiNP2xlI9_bGhvwM5VWzdh19pBc,13526
230
231
  verifiers/v1/dialects/base.py,sha256=YZQDnasQseDXK-GHLfJLZgyHIIygS80YhD_sTjmUbfA,8126
@@ -287,7 +288,7 @@ verifiers/v1/interception/server.py,sha256=jJmAkzQpDOEQggJNwCQlJlboMZFrsoF0YpuL7
287
288
  verifiers/v1/interception/tunnel/__init__.py,sha256=eVNZJszj6myrKhx9jmJz7VRd9qlfj0AUvUZwI2bXEj8,893
288
289
  verifiers/v1/interception/tunnel/base.py,sha256=DZB6uPLwM4Qy7n7m0vKeyg-Px87MhsmpYTpe2vpPEng,1998
289
290
  verifiers/v1/interception/tunnel/custom.py,sha256=yL4UbGf4bAuMsxk42mEP-t1wITQCT8BE9FHQIz0qRMA,1708
290
- verifiers/v1/interception/tunnel/prime.py,sha256=PP3Be3PfbfsLlhF4FfpsZt7IkiYiC1DBF-EidlKQio4,2530
291
+ verifiers/v1/interception/tunnel/prime.py,sha256=0CjoQ2YoxwRUFRU1BftNgqGvqFlbM5is8KaWGrD7eQ0,2531
291
292
  verifiers/v1/judges/__init__.py,sha256=MUIBWcx6c70BykrTDHhB0ITIyJPxXe6xDBou4VM7jDQ,286
292
293
  verifiers/v1/judges/reference.py,sha256=iEVHw-iJ6dbwLOOqrvWrEMJOcUEYJ0YspT04rZz7Sd4,3868
293
294
  verifiers/v1/judges/reference.txt,sha256=Ej35kGXiT2uJ0LcHIeez49rCS_9uHmCQKoVdn93V6A8,353
@@ -299,9 +300,9 @@ verifiers/v1/mcp/server.py,sha256=dRBxytFgS6rv1BS6t5EgBd3xeF6U1nTyt0ArDCLk3VE,12
299
300
  verifiers/v1/mcp/toolset.py,sha256=T8Ioiq4wphxVOTHUDf28ZoQBM2ZYEevGnGgw8TFEMiA,1002
300
301
  verifiers/v1/runtimes/__init__.py,sha256=Tt1x_rS08R_5f6vkqW0EmrhXRXQAp23czHyoFrWSPHM,2671
301
302
  verifiers/v1/runtimes/base.py,sha256=lqTTnJk-NcN18QTP9lDIqkSKRCw8lK_jZXN1THpGl98,17175
302
- verifiers/v1/runtimes/limiters.py,sha256=ZjjYUFEt20yH8ggL5uU9w9U500AOB44qSUTQ7bYyh0c,2730
303
- verifiers/v1/runtimes/modal.py,sha256=QMNVQtiTTN6ZDiS-TA3nJZGfZw_HVlkuxHvPx55Yk8Q,12591
304
- verifiers/v1/runtimes/prime.py,sha256=QtDtkZITorFdHyWSr-i37RB7HStZ9pxp-I-I6zkkZmA,16240
303
+ verifiers/v1/runtimes/limiters.py,sha256=F8HD3Sbn7XFum9cblUzvtDnyVKA1LnoYwi_J-GHUIzQ,2814
304
+ verifiers/v1/runtimes/modal.py,sha256=XLCXLXYee7-6xY9kg2CFd-o83ztwYZESW1WAa5bJZ3A,12591
305
+ verifiers/v1/runtimes/prime.py,sha256=FZ0ePwcpooWcviUbobbpJjE9bF_61cXGe4BWjCYlMqw,16240
305
306
  verifiers/v1/runtimes/subprocess.py,sha256=4WBdLJrDF347FDGqTcazBVb2g_jRw0Q0r3AlCbqy9PE,7552
306
307
  verifiers/v1/runtimes/docker/__init__.py,sha256=LvClYPNVchcrnW2wVbOhi8-bzXUu3E4hdwZv2iNMrXQ,20346
307
308
  verifiers/v1/runtimes/docker/egress.py,sha256=37UXgNX9gnoLxjMg4KZaJIGoApCAM6MK22eJGNoMi38,14514
@@ -339,8 +340,8 @@ verifiers/v1/utils/platform.py,sha256=56Ixmk1SER6q5LvzyYcA0hgzGpzhxKZL8Hiqp4c_XV
339
340
  verifiers/v1/utils/retries.py,sha256=Y2ZgrAjn-qNkRKZP_RVNL_7EK0iaRcZeCa404lpGYi4,5417
340
341
  verifiers/v1/utils/score.py,sha256=-R5Cog14r_tJ6Q_oOfGr5VPAm2CCUhofYZB6B9lm4wM,5787
341
342
  verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
342
- verifiers-0.2.2.dev94.dist-info/METADATA,sha256=Apenwhh-Aj7Nzm8EIVuzl1-UHCvpksZZQSOnGmCg4rE,4545
343
- verifiers-0.2.2.dev94.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
344
- verifiers-0.2.2.dev94.dist-info/entry_points.txt,sha256=v6v0QT9vVExnfn4br42MostI_o3f3ZbWYlTormz-U3g,515
345
- verifiers-0.2.2.dev94.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
346
- verifiers-0.2.2.dev94.dist-info/RECORD,,
343
+ verifiers-0.2.2.dev96.dist-info/METADATA,sha256=PcK9Bjal72b0WZkEVAOhTTlLqkJUhtpMvEl2xhEJlPA,4545
344
+ verifiers-0.2.2.dev96.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
345
+ verifiers-0.2.2.dev96.dist-info/entry_points.txt,sha256=v6v0QT9vVExnfn4br42MostI_o3f3ZbWYlTormz-U3g,515
346
+ verifiers-0.2.2.dev96.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
347
+ verifiers-0.2.2.dev96.dist-info/RECORD,,