-
Notifications
You must be signed in to change notification settings - Fork 665
feat(v1): Harbor separate verifier environments #2152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b22fae7
9867b62
b5926a7
5f351aa
345dd05
3ea7201
7377697
d1f804e
0dd56a8
5fe32f4
fa037f0
b1d99d0
9166685
b3b85f4
f29df85
1213839
be6573f
59b9bfd
01f0e76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
macroscopeapp[bot] marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,16 @@ | ||
| from verifiers.v1.tasksets.harbor.env import HarborEnv, HarborEnvConfig | ||
| from verifiers.v1.tasksets.harbor.taskset import ( | ||
| HarborConfig, | ||
| HarborData, | ||
| HarborTask, | ||
| HarborTaskset, | ||
| ) | ||
|
|
||
| __all__ = ["HarborConfig", "HarborData", "HarborTask", "HarborTaskset"] | ||
| __all__ = [ | ||
| "HarborConfig", | ||
| "HarborData", | ||
| "HarborEnv", | ||
| "HarborEnvConfig", | ||
| "HarborTask", | ||
| "HarborTaskset", | ||
| ] | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| """The harbor taskset's own env: the single solver seat, plus separate-verifier | ||
| grading for tasks that declare ``[verifier].environment_mode = "separate"``. | ||
|
|
||
| The default env for harbor runs (the taskset package exports it). A shared-verifier | ||
| task runs exactly as under the single-agent env: one `agent` trace, graded in the | ||
| box it worked in. A separate-verifier task is graded by `finalize` instead: the | ||
| solver's declared artifacts travel (collected by its task `finalize` while its box | ||
| is alive), a fresh box is provisioned from the task's verifier declaration, | ||
| `tests/` is staged there, and the verifier's rewards land on the solver's trace. | ||
| No second agent is involved — the verifier is the task's own `tests/test.sh`. | ||
| """ | ||
|
|
||
| import asyncio | ||
| import logging | ||
| from contextlib import AsyncExitStack | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| import verifiers.v1 as vf | ||
| from verifiers.v1.runtimes import RuntimeConfig, provision_runtime | ||
| from verifiers.v1.tasksets.harbor.taskset import ( | ||
| HarborTask, | ||
| verifier_box_data, | ||
| ) | ||
| from verifiers.v1.utils.artifacts import restore | ||
| from verifiers.v1.utils.compile import resolve_runtime_config | ||
| from verifiers.v1.utils.retries import backoff | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class HarborEnvConfig(vf.EnvConfig): | ||
| agent: vf.AgentConfig = vf.AgentConfig() | ||
| """The one seat — the policy under evaluation/training; pin | ||
| `--env.agent.harness.*` to choose its program or runtime.""" | ||
| verifier_runtime: RuntimeConfig | None = None | ||
| """Where a separate-verifier task grades. None derives the grading box from | ||
| the solver's runtime policy; set it (e.g. `--env.verifier-runtime.type prime | ||
| --env.verifier-runtime.vm true`) when the verifier needs different placement | ||
| than the agent.""" | ||
| verifier_retries: int = Field(2, ge=0) | ||
| """Extra attempts at provisioning-and-grading the separate box before the | ||
| episode fails. Grading is deterministic; what these retry is the | ||
| infrastructure around it (image pulls, provisioning).""" | ||
|
|
||
|
|
||
| class HarborEnv(vf.Env[HarborEnvConfig]): | ||
| async def run(self, task: vf.Task, agents: vf.Agents) -> None: | ||
| if not isinstance(task, HarborTask): | ||
| raise TypeError( | ||
| f"the harbor env runs harbor tasks; got {type(task).__name__}" | ||
| ) | ||
| if task.data.verifier is None: | ||
| await agents.agent.run(task) | ||
| return | ||
| # Resolve the verifier's box before the solve, so an impossible pairing | ||
| # (e.g. a restricted Prime verifier without vm=true) costs nothing | ||
| # rather than a full agent run. | ||
| self._verifier_config(task) | ||
| await agents.agent.run(task.graded_elsewhere()) | ||
|
|
||
| def _verifier_config(self, task: HarborTask) -> RuntimeConfig: | ||
| base = ( | ||
| self.config.verifier_runtime | ||
| if self.config.verifier_runtime is not None | ||
| else self.config.agent.runtime | ||
| ) | ||
| return resolve_runtime_config(base, HarborTask(verifier_box_data(task.data))) | ||
|
|
||
| async def finalize(self, task: vf.Task, episode: vf.Episode) -> None: | ||
| """Grade a separate-verifier task in its own box, onto the solver's trace. | ||
|
|
||
| Provision a fresh box from the task's verifier declaration, restore the | ||
| solver's collected artifacts, stage `tests/`, run the verifier, and record | ||
| its rewards (and any extra reward.json keys as metrics) on the solver's | ||
| trace. Infrastructure failures retry per `verifier_retries`; the last one | ||
| fails the episode — a grading box that can't be reached must never read | ||
| as reward 0.""" | ||
| if not isinstance(task, HarborTask) or task.data.verifier is None: | ||
| return | ||
| solution = episode.traces[0] | ||
| if not solution.ok: | ||
| return | ||
| grader = HarborTask(verifier_box_data(task.data)) | ||
| scores = await self._grade(self._verifier_config(task), grader, solution) | ||
| items = scores.items() if isinstance(scores, dict) else [("solved", scores)] | ||
| for name, value in items: | ||
| solution.record_reward(name, value) | ||
|
|
||
| async def _grade( | ||
| self, config: RuntimeConfig, grader: HarborTask, solution: vf.Trace | ||
| ) -> float | dict[str, float]: | ||
| last: Exception | None = None | ||
| for attempt in range(self.config.verifier_retries + 1): | ||
| if attempt: | ||
| delay = backoff(attempt - 1) | ||
| logger.warning( | ||
| "harbor verifier attempt %d/%d failed (%s); retrying in %.1fs", | ||
| attempt, | ||
| self.config.verifier_retries + 1, | ||
| last, | ||
| delay, | ||
| ) | ||
| await asyncio.sleep(delay) | ||
| try: | ||
| # The scoring deadline covers provisioning and grading, but not the | ||
| # box's teardown: a score already in hand must not be discarded | ||
| # because the teardown ran out the clock. | ||
| async with AsyncExitStack() as boxes: | ||
| async with asyncio.timeout(grader.data.timeout.scoring): | ||
| box = await boxes.enter_async_context(provision_runtime(config)) | ||
| await box.prepare_setup() | ||
| # Artifacts first, tests second: an artifact entry pointing | ||
| # into /tests must not survive staging, which wipes and | ||
| # rebuilds that directory. | ||
| await restore(box, solution.state.artifacts) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw at some point i think we could have smth like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, good point |
||
| await grader._stage_tests(box, wipe=True) | ||
| await box.prepare_execution([]) | ||
| scores = await grader._graded(box, solution) | ||
| return scores | ||
| except Exception as e: # noqa: BLE001 - each attempt's failure is retried | ||
| last = e | ||
|
cursor[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Teardown failure discards graded scoreMedium Severity
Reviewed by Cursor Bugbot for commit 59b9bfd. Configure here. |
||
| assert last is not None | ||
| raise last | ||


Uh oh!
There was an error while loading. Please reload this page.