diff --git a/verifiers/v1/cli/dashboard/eval.py b/verifiers/v1/cli/dashboard/eval.py index 4e14272637..c729af026b 100644 --- a/verifiers/v1/cli/dashboard/eval.py +++ b/verifiers/v1/cli/dashboard/eval.py @@ -7,7 +7,8 @@ from typing import TYPE_CHECKING from pydantic import BaseModel -from rich.console import Console, Group +from rich import get_console +from rich.console import Group from rich.markup import escape from rich.progress_bar import ProgressBar from rich.rule import Rule @@ -33,7 +34,7 @@ # For sizing pages to the terminal: detects the real terminal height/width each access (the live # view writes to the same terminal). Reused so we don't rebuild it every refresh tick. -_CONSOLE = Console() +_CONSOLE = get_console() _PAGE_SECONDS = 5.0 # rotate to the next page of rollouts this often when they overflow # The under-bar breakdown pads its label column to the Overview's widest label, so the two # `label value` grids (above and below the progress bar) line their values up. @@ -188,10 +189,9 @@ def overrides( skip: frozenset[str] = frozenset(), ) -> list[str]: """`field=value` segments for the fields the *user* customized, sorted, diffed against each - field's declared default. Not `model_fields_set`: a `--resume` run reloads its config via - `model_validate(config.toml)`, and that toml is dumped with `exclude_none` (every field), so - `model_fields_set` would flag them all. `default` is the reference instance, threaded through - recursion so a pinned nested default (`taskset.task.tools`) reads as + field's declared default. Not `model_fields_set`: a `--resume` run reloads the full saved + JSON config, so `model_fields_set` would flag every field. `default` is the reference instance, + threaded through recursion so a pinned nested default (`taskset.task.tools`) reads as unchanged. `skip` holds dotted paths (`runtime.type`).""" segments: list[str] = [] fields = type(config).model_fields @@ -782,21 +782,21 @@ def _paginate( def _render( slots: list[RunSlot], - config: EvalConfig, start: float, pager: Pager, + header: Group | Table, + runtime_type: str, push: "PushState | None" = None, tail: LogTail | None = None, ) -> Group: now = time.time() - warning = _warning(config) - header = Group(warning, Text(""), Overview(config)) if warning else Overview(config) # The --push status line (and, on Ctrl-C, the cleanup notice) appear under the rollouts. Measure # the fixed top (header + progress + rule) and the footer so the rollout rows fill what's left; # page through them (timer / arrows) when they'd overflow (else rich truncates). footers = [f for f in (_push_footer(push), _interrupt_footer()) if f is not None] footer = Group(*footers) if footers else None - top = Group(header, Progress(slots, start), Rule(style="dim")) + progress = Progress(slots, start) + top = Group(header, progress, Rule(style="dim")) reserved = len(_CONSOLE.render_lines(top)) if footer is not None: reserved += len(_CONSOLE.render_lines(footer)) @@ -804,7 +804,7 @@ def _render( if tail is not None: # --show-logs: the run's log stream in place of rollout rows parts = [ header, - Progress(slots, start), + progress, Rule(style="dim"), tail.view(rows_per_page), ] @@ -812,26 +812,13 @@ def _render( parts.append(footer) return Group(*parts) page_groups, index, count = _paginate(_groups(slots), rows_per_page, pager, now) - progress = Progress( - slots, - start, - page=(index + 1, count) if count > 1 else None, - ) + if count > 1: + progress = Progress(slots, start, page=(index + 1, count)) parts = [ header, progress, Rule(style="dim"), - Rows( - page_groups, - now, - "/".join( - dict.fromkeys( - getattr(config.env, role).runtime.type - for role in config.env.agent_harnesses() - ) - ) - or "subprocess", - ), + Rows(page_groups, now, runtime_type), ] if footer is not None: parts.append(footer) @@ -846,13 +833,24 @@ async def dashboard( push: "PushState | None" = None, ): pager = Pager() + warning = _warning(config) + header = Group(warning, Text(""), Overview(config)) if warning else Overview(config) + runtime_type = ( + "/".join( + dict.fromkeys( + getattr(config.env, role).runtime.type + for role in config.env.agent_harnesses() + ) + ) + or "subprocess" + ) tail = ( LogTail(attempt_log_file(output_path(config))) if config.rich is not None and config.rich.show_logs else None ) async with live_view( - lambda: _render(slots, config, start, pager, push, tail), - on_key=pager.on_key, + lambda: _render(slots, start, pager, header, runtime_type, push, tail), + on_key=None if tail is not None else pager.on_key, ): yield diff --git a/verifiers/v1/cli/dashboard/replay.py b/verifiers/v1/cli/dashboard/replay.py index 0df3e304ae..3cb67976cf 100644 --- a/verifiers/v1/cli/dashboard/replay.py +++ b/verifiers/v1/cli/dashboard/replay.py @@ -40,7 +40,7 @@ class ReplayProgress(TaskProgress): def _render( - states: list[TaskProgress], taskset_name: str, source: str, out: str, start: float + states: list[ReplayProgress], taskset_name: str, source: str, out: str, start: float ) -> Group: overview = Table.grid(padding=(0, 2)) overview.add_column(style="dim") @@ -57,11 +57,7 @@ def _render( if s.state == "pending": # show only in-flight/done rows (like eval/validate) continue label = f"name={s.name[:40]}" if s.name else f"idx={s.idx}" - parts = [ - p - for p in (s.state if s.state in _DONE else "", getattr(s, "detail", "")) - if p - ] + parts = [p for p in (s.state if s.state in _DONE else "", s.detail) if p] result = " ".join(parts) + " ·" if parts else "" elapsed = format_time((s.end or now) - s.start) if s.start else "" rows.add_row( @@ -75,7 +71,7 @@ def _render( @contextlib.asynccontextmanager async def replay_dashboard( - states: list[TaskProgress], taskset_name: str, source: str, out: str, start: float + states: list[ReplayProgress], taskset_name: str, source: str, out: str, start: float ): """Refresh the live replay view until the `with` block exits, then a final frame.""" async with live_view(lambda: _render(states, taskset_name, source, out, start)): diff --git a/verifiers/v1/cli/debug.py b/verifiers/v1/cli/debug.py index 37c5171a1f..c98be40d65 100644 --- a/verifiers/v1/cli/debug.py +++ b/verifiers/v1/cli/debug.py @@ -240,7 +240,8 @@ async def debug_task(task: Task, config: DebugConfig) -> tuple[Trace, bool]: trace.timing.agent.start = time.time() debug.update(await run_action(runtime, config)) trace.timing.agent.end = time.time() - if not debug.get("ok"): + trace.ok = bool(debug["ok"]) + if not trace.ok: record_action_failure(trace, debug) trace.stop(str(debug["reason"])) except asyncio.CancelledError as e: @@ -282,7 +283,7 @@ async def run_debug(config: DebugConfig) -> list[Trace]: ) out = output_path(config) - save_config(config, out) + save_config(config, out, "debug.json") logger.info( "debugging %d task(s) from %s on the %s runtime", len(tasks), diff --git a/verifiers/v1/cli/eval/main.py b/verifiers/v1/cli/eval/main.py index dd7c6153dd..3af8baf613 100644 --- a/verifiers/v1/cli/eval/main.py +++ b/verifiers/v1/cli/eval/main.py @@ -11,7 +11,6 @@ from verifiers.v1.cli.eval.runner import run_eval from verifiers.v1.cli.output import ( TRACES_FILE, - config_digest, create_attempt_log_dir, output_path, saved_config_path, @@ -69,7 +68,7 @@ def main(argv: list[str] | None = None) -> None: ] # let prime-pydantic-config render help/errors config = cli(config_type) # A named run directory is re-entered only by `--resume` or wiped by `--clean`: any - # other write into it — the dry-run config.toml included, which would clobber the + # other write into it — the dry-run config included, which would clobber the # config a resume typically re-runs — would overwrite the previous run. run_path = output_path(config) if config.clean and not config.resume and run_path.exists(): @@ -97,10 +96,12 @@ def main(argv: list[str] | None = None) -> None: f"--resume: no saved config under {run_path} - not a run dir" ) with plugin_errors(): - saved = config_type.model_validate(json.loads(saved_path.read_text())) - if config_digest(saved) != config_digest(config): - saved_dump = saved.model_dump(mode="json") - current_dump = config.model_dump(mode="json") + saved = config_type.model_validate_json(saved_path.read_text()) + saved_dump = saved.model_dump(mode="json") + current_dump = config.model_dump(mode="json") + saved_json = json.dumps(saved_dump, sort_keys=True, separators=(",", ":")) + current_json = json.dumps(current_dump, sort_keys=True, separators=(",", ":")) + if saved_json != current_json: changed = sorted( key for key in set(saved_dump) | set(current_dump) @@ -114,12 +115,12 @@ def main(argv: list[str] | None = None) -> None: ) if config.dry_run: # resolved + validated; write it to the output dir and exit setup_logging("DEBUG" if config.verbose else "INFO") - logger.info("wrote config to %s", write_config(config, output_path(config))) + logger.info("wrote config to %s", write_config(config, run_path)) return # Always tee this attempt's logs to `logs/attempt_/eval.log` (`logs/latest` # points there) — in server mode (the default) the workers write there too, and # `--rich.show-logs` tails it live. - log_file = str(create_attempt_log_dir(output_path(config)) / "eval.log") + log_file = str(create_attempt_log_dir(run_path) / "eval.log") level = "DEBUG" if config.verbose else "INFO" setup_logging(level, log_file=log_file, console=config.rich is None) # First Ctrl-C / SIGTERM warns and raises KeyboardInterrupt so a killed/timed-out eval still diff --git a/verifiers/v1/cli/eval/resume.py b/verifiers/v1/cli/eval/resume.py index a51ee79b4d..eb6a759809 100644 --- a/verifiers/v1/cli/eval/resume.py +++ b/verifiers/v1/cli/eval/resume.py @@ -4,12 +4,12 @@ it. `load` keeps the good saved rollouts and re-runs what's owed: missing rollouts (never written) and errored ones (dropped and redone). -A saved episode is matched to a selected task by hashing `episode.task.data`. -Tasks with identical data are interchangeable, a task whose data changed since the -interrupted run re-runs, and nothing depends on `data.idx`. +A saved episode is matched to a selected task by its persisted `episode.task.hash`, +falling back to hashing `episode.task.data` for older rows without one. Tasks with +identical data are interchangeable, a task whose data changed since the interrupted +run re-runs, and nothing depends on `data.idx`. """ -import json from collections import Counter, defaultdict from collections.abc import Callable from pathlib import Path @@ -47,17 +47,21 @@ def load( if not line.strip(): continue try: - try: - row = from_json(line) - except ValueError: - row = json.loads(line) + row = from_json(line) if "traces" not in row: continue - key = task_key(row["task"]["data"]) + task = row["task"] + if not isinstance(task, dict): + continue + key = task.get("hash") + if key is None: + key = task_key(task["data"]) except (ValueError, KeyError, IndexError, TypeError): # A torn final line (the run died mid-write) or a foreign shape # is not a keepable rollout — it's owed again, never a crash. continue + if not isinstance(key, str): + continue if key not in targets or len(good[key]) >= targets[key]: continue try: @@ -83,11 +87,3 @@ def load( tmp.write_bytes(b"".join(keep)) tmp.replace(path) return episodes, owed - - -def nothing_to_resume_msg(resume_dir: Path, num_tasks: int, num_rollouts: int) -> str: - """Shown (before exit 0) when every selected rollout already completed.""" - return ( - f"nothing to resume in {resume_dir}: all {num_tasks}x{num_rollouts} rollouts " - f"already completed without error" - ) diff --git a/verifiers/v1/cli/eval/runner.py b/verifiers/v1/cli/eval/runner.py index 56043d0000..2b593fc5de 100644 --- a/verifiers/v1/cli/eval/runner.py +++ b/verifiers/v1/cli/eval/runner.py @@ -170,7 +170,10 @@ async def run_eval(config: EvalConfig) -> list[Episode]: loaded, owed = resume.load(out, keys, config.num_rollouts, complete) finished = [cast(Episode, episode) for episode in loaded] if not owed: # already complete - report it and exit successfully - print(resume.nothing_to_resume_msg(out, len(tasks), config.num_rollouts)) + print( + f"nothing to resume in {out}: all {len(tasks)}x{config.num_rollouts} " + "rollouts already completed without error" + ) raise SystemExit(0) counts = distribute(keys, owed, config.num_rollouts) plan = [(task, n) for task, n in zip(tasks, counts) if n] diff --git a/verifiers/v1/cli/init.py b/verifiers/v1/cli/init.py index d37e98fc86..727eef82fe 100644 --- a/verifiers/v1/cli/init.py +++ b/verifiers/v1/cli/init.py @@ -24,14 +24,10 @@ def _names(name: str) -> tuple[str, str, str, str]: return dash, pkg, stem, prefix -def _write(path: Path, content: str, force: bool) -> bool: - if path.exists() and not force: - print(f" skip {path} (exists)") - return False +def _write(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content) print(f" write {path}") - return True def _pyproject(dash: str, pkg: str) -> str: @@ -67,7 +63,6 @@ def _taskset_py(pkg: str, prefix: str, *, add_tool: bool) -> str: local_imports: list[str] = [] task_config_fields = "" task_decls = "" - state = "vf.State" if add_tool: local_imports.append(f"from {pkg}.servers.tool import {prefix}Toolset") task_config_fields += "\n tools: vf.ToolsetConfig = vf.ToolsetConfig()" @@ -79,24 +74,18 @@ def _taskset_py(pkg: str, prefix: str, *, add_tool: bool) -> str: ) if local_imports: imports += "\n\n" + "\n".join(local_imports) - methods_block = "" - has_task_config = add_tool task_config = ( f"\n\nclass {prefix}TaskConfig(vf.TaskConfig):\n" ' """Knobs the task reads from ``self.config``; configure them under ' f'``--env.taskset.task.*``."""{task_config_fields}\n' - if has_task_config + if add_tool else "" ) task_generic = ( - f"{prefix}Data, {state}, {prefix}TaskConfig" - if has_task_config - else f"{prefix}Data" + f"{prefix}Data, vf.State, {prefix}TaskConfig" if add_tool else f"{prefix}Data" ) config_body = ' num_tasks: int = 5\n """How many tasks to build."""\n' + ( - f" task: {prefix}TaskConfig = {prefix}TaskConfig()\n" - if has_task_config - else "" + f" task: {prefix}TaskConfig = {prefix}TaskConfig()\n" if add_tool else "" ) return f'''\ {imports} @@ -107,7 +96,7 @@ class {prefix}Data(vf.TaskData): {task_config} class {prefix}Task(vf.Task[{task_generic}]): - """Rewards, hooks, and servers, with row data available on ``self.data``."""{task_decls}{methods_block} + """Rewards, hooks, and servers, with row data available on ``self.data``."""{task_decls} @vf.reward(weight=1.0) async def reward(self, trace: vf.Trace) -> float: raise NotImplementedError("Score the rollout and return a float (e.g. in [0, 1]).") @@ -223,25 +212,21 @@ def scaffold(config: InitConfig) -> Path: ) print(f"scaffolding v1 environment {dash!r} in {env_dir}") - _write(env_dir / "pyproject.toml", _pyproject(dash, pkg), config.force) + _write(env_dir / "pyproject.toml", _pyproject(dash, pkg)) _write( env_dir / "README.md", _readme(dash, pkg, add_tool=config.add_tool, add_harness=config.add_harness), - config.force, - ) - _write( - pkg_dir / "__init__.py", _init_py(pkg, prefix, config.add_harness), config.force ) + _write(pkg_dir / "__init__.py", _init_py(pkg, prefix, config.add_harness)) _write( pkg_dir / "taskset.py", _taskset_py(pkg, prefix, add_tool=config.add_tool), - config.force, ) if config.add_harness: - _write(pkg_dir / "harness.py", _harness_py(prefix), config.force) + _write(pkg_dir / "harness.py", _harness_py(prefix)) if config.add_tool: - _write(pkg_dir / "servers" / "__init__.py", "", config.force) - _write(pkg_dir / "servers" / "tool.py", _tool_py(stem, prefix), config.force) + _write(pkg_dir / "servers" / "__init__.py", "") + _write(pkg_dir / "servers" / "tool.py", _tool_py(stem, prefix)) print(f"\ndone. next:\n uv pip install -e {env_dir}\n uv run eval {dash} -n 3") return env_dir @@ -260,8 +245,6 @@ def main(argv: list[str] | None = None) -> None: sys.argv = [sys.argv[0], *argv] config = cli(InitConfig) - if not config.name: - raise SystemExit(USAGE) scaffold(config) diff --git a/verifiers/v1/cli/output.py b/verifiers/v1/cli/output.py index be9842d4a8..cfef9fe48f 100644 --- a/verifiers/v1/cli/output.py +++ b/verifiers/v1/cli/output.py @@ -1,16 +1,15 @@ -"""On-disk output: traces.jsonl (one rollout episode per line) + config.toml. +"""On-disk output: traces.jsonl (one rollout episode per line) + configs/.json. Each line is an `Episode` — the episode's standing (`id`/`env`/`errors`) inlined next to its flat, self-contained traces — so an episode persists whole or not at all: a torn line is the whole episode owed on resume, and a failure before any trace minted still leaves -its errors on disk. config.toml is the run's resolved config in the format the -CLI reads (`@ config.toml`), so a run is re-runnable from its own output. Lines +its errors on disk. The JSON file is the run's resolved config in the format the +CLI reads (`@ configs/.json`), so a run is re-runnable from its own output. Lines append as episodes complete, so results are durable mid-run. Files written by this surface contain episodes only. """ import asyncio -import hashlib import json import os from functools import cache @@ -68,13 +67,6 @@ def attempt_log_file(run_dir: Path) -> Path: return latest / "eval.log" -def config_digest(config: BaseModel) -> str: - """Canonical hash of a resolved config's full model dump (nulls included).""" - dump = config.model_dump(mode="json") - canonical = json.dumps(dump, sort_keys=True, separators=(",", ":")) - return hashlib.sha256(canonical.encode()).hexdigest() - - def saved_config_path(run_dir: Path) -> Path | None: """The run's saved resolved config (`configs/.json`), None if absent.""" candidates = ( @@ -171,9 +163,8 @@ async def persist() -> None: async def append_trace( results_dir: Path, trace: Trace, lock: asyncio.Lock, env: str = "" ) -> None: - """Append one finished trace as a single-agent rollout episode — the writers that - complete trace-at-a-time (eval runners, gepa, replay) all go - through here.""" + """Append one finished trace as a single-agent rollout episode — debug and replay, + which complete trace-at-a-time, both go through here.""" episode = Episode( env=EnvInfo(id=env), task=trace.task, diff --git a/verifiers/v1/cli/resolve.py b/verifiers/v1/cli/resolve.py index dc2b671943..a5c8029008 100644 --- a/verifiers/v1/cli/resolve.py +++ b/verifiers/v1/cli/resolve.py @@ -46,7 +46,7 @@ def references_config_file(argv: list[str]) -> bool: return any(arg.startswith("@") for arg in argv) -def extract_id(argv: list[str], field: str, default: str = "") -> str: +def extract_id(argv: list[str], field: str) -> str: """The chosen `.id` from `--.id ` (or `=`) on the CLI, before the typed parse (the positional taskset shorthand is applied upstream). Two occurrences naming different ids are refused — narrowing would pin the first @@ -70,7 +70,7 @@ def extract_id(argv: list[str], field: str, default: str = "") -> str: f"{flag} is set twice, with different ids: {distinct[0]!r} and " f"{distinct[1]!r}{hint}; drop one" ) - return found[0] if found else default + return found[0] if found else "" def narrow_taskset_config(base: type[ConfigT], taskset_id: str | None) -> type[ConfigT]: diff --git a/verifiers/v1/cli/validate.py b/verifiers/v1/cli/validate.py index 4d809dd737..b569deef92 100644 --- a/verifiers/v1/cli/validate.py +++ b/verifiers/v1/cli/validate.py @@ -51,7 +51,7 @@ USAGE = ( "usage: uv run validate [] [--only-setup | --only-gold] " "[-o ] [--runtime.type subprocess] [options] [@ file.toml]\n" - " uv run validate @ /config.toml --resume (re-run missing/errored/timed-out tasks)\n" + " uv run validate @ /configs/validate.json --resume (re-run missing/errored/timed-out tasks)\n" " runs persisted gold and setup-only checks per task (no model)" ) @@ -119,10 +119,7 @@ def load_results( try: row = from_json(line) except ValueError: - try: - row = json.loads(line) - except (json.JSONDecodeError, UnicodeDecodeError): - continue + continue if not isinstance(row, dict): continue key = row.get("task_key") @@ -222,11 +219,11 @@ def _row( } -async def _run_gold(task: Task, config: ValidateConfig) -> ResultRow: +async def _run_check(task: Task, config: ValidateConfig, mode: str) -> ResultRow: start = time.time() runtime = make_runtime( resolve_runtime_config(config.runtime, task), - name=f"validate-gold-{task.data.idx}-{uuid4().hex[:8]}", + name=f"validate-{mode}-{task.data.idx}-{uuid4().hex[:8]}", ) setup_timeout = ( config.timeout.setup @@ -253,51 +250,11 @@ async def _run_gold(task: Task, config: ValidateConfig) -> ResultRow: invoke(task.setup, {"trace": trace, "runtime": runtime}), setup_timeout, ) - valid = await asyncio.wait_for(task.validate(runtime), config.timeout.total) - except Exception as e: # noqa: BLE001 - validation reports plugin failures per task - exc = e - finally: - try: - await runtime.stop() - except Exception: - logger.warning( - "runtime teardown failed (task %s)", task.data.idx, exc_info=True - ) - return _row(task, "gold", valid, exc, start) - - -async def _run_setup(task: Task, config: ValidateConfig) -> ResultRow: - start = time.time() - runtime = make_runtime( - resolve_runtime_config(config.runtime, task), - name=f"validate-setup-{task.data.idx}-{uuid4().hex[:8]}", - ) - setup_timeout = ( - config.timeout.setup - if config.timeout.setup is not None - else task.data.timeout.setup - ) - valid, exc = False, None - try: - runtime.env = dict(task.runtime_env()) - trace = Trace( - task=TraceTask( - type=type(task).__name__, data=task.data, key=task.key, hash=task.hash - ), - state=state_cls(type(task))(), - # No agent runs here — the info only records the runtime policy. - agent=vf.AgentInfo( - config=vf.AgentConfig(runtime=config.runtime), - name="validate", - trainable=False, - ), - ) - await runtime.start() - await asyncio.wait_for( - invoke(task.setup, {"trace": trace, "runtime": runtime}), - setup_timeout, + valid = ( + await asyncio.wait_for(task.validate(runtime), config.timeout.total) + if mode == "gold" + else True ) - valid = True except Exception as e: # noqa: BLE001 - validation reports plugin failures per task exc = e finally: @@ -307,7 +264,7 @@ async def _run_setup(task: Task, config: ValidateConfig) -> ResultRow: logger.warning( "runtime teardown failed (task %s)", task.data.idx, exc_info=True ) - return _row(task, "setup", valid, exc, start) + return _row(task, mode, valid, exc, start) def _all_reason(rows: list[ResultRow]) -> str: @@ -330,8 +287,8 @@ def _all_error(rows: list[ResultRow]) -> tuple[str | None, str | None]: async def _run_all(task: Task, config: ValidateConfig) -> ResultRow: start = time.time() - gold = await _run_gold(task, config) - setup = await _run_setup(task, config) + gold = await _run_check(task, config, "gold") + setup = await _run_check(task, config, "setup") rows = [gold, setup] error, error_type = _all_error(rows) return { @@ -349,11 +306,12 @@ async def _run_all(task: Task, config: ValidateConfig) -> ResultRow: async def _validate_task(task: Task, config: ValidateConfig) -> ResultRow: - if config.only_gold: - return await _run_gold(task, config) - if config.only_setup: - return await _run_setup(task, config) - return await _run_all(task, config) + mode = validation_mode(config) + return ( + await _run_all(task, config) + if mode == "all" + else await _run_check(task, config, mode) + ) async def run_validate(config: ValidateConfig) -> list[dict]: diff --git a/verifiers/v1/configs/cli/__init__.py b/verifiers/v1/configs/cli/__init__.py index 3acc11d1c0..f9efe5ee2e 100644 --- a/verifiers/v1/configs/cli/__init__.py +++ b/verifiers/v1/configs/cli/__init__.py @@ -1,8 +1 @@ """The CLI entrypoints' run configs — each `uv run `'s parsed tree.""" - -from verifiers.v1.configs.cli.debug import DebugConfig -from verifiers.v1.configs.cli.eval import EvalConfig -from verifiers.v1.configs.cli.init import InitConfig -from verifiers.v1.configs.cli.validate import ValidateConfig - -__all__ = ["DebugConfig", "EvalConfig", "InitConfig", "ValidateConfig"] diff --git a/verifiers/v1/configs/cli/debug.py b/verifiers/v1/configs/cli/debug.py index b59f52aca2..dc32b4fd2f 100644 --- a/verifiers/v1/configs/cli/debug.py +++ b/verifiers/v1/configs/cli/debug.py @@ -43,7 +43,7 @@ class DebugConfig(BaseConfig): output_dir: Path | None = Field( None, validation_alias=AliasChoices("output_dir", "o") ) - """Where to write `config.toml` and `traces.jsonl`. None = a fresh per-run dir.""" + """Where to write `configs/debug.json` and `traces.jsonl`. None = a fresh per-run dir.""" @property def name(self) -> str: diff --git a/verifiers/v1/configs/cli/eval.py b/verifiers/v1/configs/cli/eval.py index 223e12af67..02fab474d6 100644 --- a/verifiers/v1/configs/cli/eval.py +++ b/verifiers/v1/configs/cli/eval.py @@ -65,8 +65,8 @@ class EvalConfig(BaseConfig): worker's episode bound — the path prime-rl trains through. `--no-serve` runs the rollouts in-process instead.""" run: RunConfig = Field(default_factory=RunConfig) - """Run identity: `run.name` names the run directory under `output_dir`, `run.id` is - stamped on traces.""" + """Run identity: `run.name` is the display name, `run.dir` names the directory + under `output_dir`, and `run.id` is stamped on traces.""" model: str = Field( "deepseek/deepseek-v4-flash", validation_alias=AliasChoices("model", "m") ) @@ -100,7 +100,7 @@ class EvalConfig(BaseConfig): """Log at debug level instead of the default info.""" dry_run: bool = Field(False, exclude=True) """Resolve + validate the config and dump it, then exit. Excluded from the saved - config so re-running `@ config.toml` (or resuming/replaying the dir) actually runs.""" + config so re-running `@ configs/eval.json` (or resuming/replaying the dir) actually runs.""" clean: bool = Field(False, exclude=True) """Delete the run directory (`output_dir / run.dir`) before running, overwriting a previous run's results. Excluded from the saved config.""" @@ -115,18 +115,14 @@ class EvalConfig(BaseConfig): output_dir: Path = Field( Path("outputs"), validation_alias=AliasChoices("output_dir", "o") ) - """Directory that groups related runs. The run itself (config.toml + traces.jsonl) - writes to `output_dir / run.name`.""" + """Directory that groups related runs. The run itself (`configs/eval.json` + + `traces.jsonl`) writes to `output_dir / run.dir`.""" resume: bool = Field(False, exclude=True) """Re-run the run's missing/errored rollouts in place instead of starting fresh. The run dir comes from the resolved config (`output_dir / run.dir`), so resume with the - run's own config — e.g. `uv run eval @ /config.toml --resume`. Excluded + run's own config — e.g. `uv run eval @ /configs/eval.json --resume`. Excluded from the saved config.""" - @property - def env_id(self) -> str: - return self.env.env_id or "" - @model_validator(mode="before") @classmethod def _resolve_env(cls, data): diff --git a/verifiers/v1/configs/cli/init.py b/verifiers/v1/configs/cli/init.py index ed069039ce..1b1da96e20 100644 --- a/verifiers/v1/configs/cli/init.py +++ b/verifiers/v1/configs/cli/init.py @@ -5,7 +5,7 @@ class InitConfig(BaseConfig): - name: str = "" + name: str = Field(min_length=1) """The new environment id, e.g. `my-task-v1` (positional: `init my-task-v1`).""" path: str = Field("./environments", validation_alias=AliasChoices("path", "p")) """Parent directory the package is created in (default `./environments`).""" diff --git a/verifiers/v1/configs/cli/replay.py b/verifiers/v1/configs/cli/replay.py index 78304f9e3b..db27f95d90 100644 --- a/verifiers/v1/configs/cli/replay.py +++ b/verifiers/v1/configs/cli/replay.py @@ -40,8 +40,9 @@ class ReplayConfig(BaseConfig): output_dir: Path | None = Field( None, validation_alias=AliasChoices("output_dir", "o") ) - """Where to write the re-scored run (config.toml + traces.jsonl). None = a fresh per-run - dir under `outputs/--replay/` (so a replay never overwrites the source run).""" + """Where to write the re-scored run (`configs/replay.json` + `traces.jsonl`). + None = a fresh per-run dir under `outputs/--replay/` (so a replay + never overwrites the source run).""" @property def name(self) -> str: diff --git a/verifiers/v1/configs/cli/validate.py b/verifiers/v1/configs/cli/validate.py index 8cad68da56..a562945464 100644 --- a/verifiers/v1/configs/cli/validate.py +++ b/verifiers/v1/configs/cli/validate.py @@ -50,13 +50,14 @@ class ValidateConfig(BaseConfig): output_dir: Path = Field( Path("outputs"), validation_alias=AliasChoices("output_dir", "o") ) - """Directory that groups related runs. The run (config.toml, results.jsonl, - summary.json, validate.log) writes to `output_dir / run.dir`.""" + """Directory that groups related runs. The run (`configs/validate.json`, + `results.jsonl`, `summary.json`, `logs/validate.log`) writes to + `output_dir / run.dir`.""" resume: bool = Field(False, exclude=True) """Re-run the run's missing, errored, and timed-out tasks in place. The run dir comes from the resolved config (`output_dir / run.dir`), so resume with the run's own - config — e.g. `uv run validate @ /config.toml --resume`. Excluded from the - saved config.""" + config — e.g. `uv run validate @ /configs/validate.json --resume`. + Excluded from the saved config.""" clean: bool = Field(False, exclude=True) """Delete the run directory (`output_dir / run.dir`) before running, overwriting a previous run's results. Excluded from the saved config.""" diff --git a/verifiers/v1/gepa/config.py b/verifiers/v1/gepa/config.py index b49754a43f..bb30ba42c5 100644 --- a/verifiers/v1/gepa/config.py +++ b/verifiers/v1/gepa/config.py @@ -36,8 +36,8 @@ def _resolve_env(cls, data): return resolve_env_field(data, narrowed_env_annotation(cls)) run: RunConfig = Field(default_factory=RunConfig) - """Run identity: `run.name` names the run directory under `output_dir`; auto-generated - like an eval's when unset.""" + """Run identity: `run.name` is the display name and `run.dir` names the directory + under `output_dir`; both auto-generate like an eval's when unset.""" model: str = Field( "deepseek/deepseek-v4-flash", validation_alias=AliasChoices("model", "m") ) @@ -78,13 +78,14 @@ def _resolve_env(cls, data): output_dir: Path = Field( Path("outputs"), validation_alias=AliasChoices("output_dir", "o") ) - """Directory that groups related runs. The run (config.toml + the streamed traces.jsonl, - alongside GEPA's own candidates.json / run_log.json) writes to `output_dir / run.name`.""" + """Directory that groups related runs. The run (`configs/gepa.json` + the streamed + `traces.jsonl`, alongside GEPA's own `candidates.json` / `run_log.json`) writes to + `output_dir / run.dir`.""" save_results: bool = True verbose: bool = Field(False, validation_alias=AliasChoices("verbose", "v")) dry_run: bool = Field(False, exclude=True) """Resolve + validate the config and dump it, then exit. Excluded from the - saved config so re-running `@ config.toml` runs for real.""" + saved config so re-running `@ configs/gepa.json` runs for real.""" @model_validator(mode="after") def auto_setup_run_name(self):