verifiers 0.2.2.dev6__py3-none-any.whl → 0.2.2.dev7__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.
Files changed (84) hide show
  1. verifiers/v1/__init__.py +38 -16
  2. verifiers/v1/agent.py +476 -0
  3. verifiers/v1/cli/__init__.py +0 -1
  4. verifiers/v1/cli/dashboard/__init__.py +0 -2
  5. verifiers/v1/cli/dashboard/eval.py +298 -153
  6. verifiers/v1/cli/debug.py +11 -6
  7. verifiers/v1/cli/eval/__init__.py +0 -1
  8. verifiers/v1/cli/eval/main.py +32 -18
  9. verifiers/v1/cli/eval/resume.py +66 -47
  10. verifiers/v1/cli/eval/runner.py +64 -66
  11. verifiers/v1/cli/gepa.py +36 -16
  12. verifiers/v1/cli/init.py +3 -3
  13. verifiers/v1/cli/output.py +80 -36
  14. verifiers/v1/cli/replay.py +55 -41
  15. verifiers/v1/cli/resolve.py +77 -46
  16. verifiers/v1/cli/serve.py +17 -9
  17. verifiers/v1/cli/validate.py +14 -6
  18. verifiers/v1/clients/__init__.py +0 -2
  19. verifiers/v1/clients/client.py +2 -2
  20. verifiers/v1/configs/__init__.py +0 -2
  21. verifiers/v1/configs/env.py +169 -0
  22. verifiers/v1/configs/eval.py +14 -6
  23. verifiers/v1/configs/init.py +1 -1
  24. verifiers/v1/configs/replay.py +12 -5
  25. verifiers/v1/configs/serve.py +1 -1
  26. verifiers/v1/decorators.py +7 -16
  27. verifiers/v1/dialects/__init__.py +1 -4
  28. verifiers/v1/dialects/base.py +0 -21
  29. verifiers/v1/env.py +450 -323
  30. verifiers/v1/envs/__init__.py +0 -0
  31. verifiers/v1/envs/agentic_judge/__init__.py +3 -0
  32. verifiers/v1/envs/agentic_judge/env.py +174 -0
  33. verifiers/v1/envs/best_of_n/__init__.py +3 -0
  34. verifiers/v1/envs/best_of_n/env.py +44 -0
  35. verifiers/v1/envs/single_agent/__init__.py +3 -0
  36. verifiers/v1/envs/single_agent/env.py +27 -0
  37. verifiers/v1/episode.py +53 -55
  38. verifiers/v1/errors.py +8 -2
  39. verifiers/v1/gepa/__init__.py +0 -7
  40. verifiers/v1/gepa/adapter.py +57 -40
  41. verifiers/v1/gepa/config.py +23 -10
  42. verifiers/v1/gepa/dataset.py +0 -15
  43. verifiers/v1/gepa/runner.py +8 -12
  44. verifiers/v1/graph.py +0 -4
  45. verifiers/v1/harness.py +22 -15
  46. verifiers/v1/harnesses/bash/harness.py +3 -3
  47. verifiers/v1/harnesses/null/harness.py +1 -0
  48. verifiers/v1/harnesses/terminus_2/harness.py +1 -1
  49. verifiers/v1/interception/__init__.py +1 -11
  50. verifiers/v1/interception/server.py +35 -4
  51. verifiers/v1/interception/tunnel/__init__.py +0 -9
  52. verifiers/v1/judge.py +9 -1
  53. verifiers/v1/judges/__init__.py +0 -2
  54. verifiers/v1/legacy.py +27 -15
  55. verifiers/v1/loaders.py +111 -8
  56. verifiers/v1/mcp/__init__.py +0 -2
  57. verifiers/v1/mcp/launch.py +4 -5
  58. verifiers/v1/push.py +81 -55
  59. verifiers/v1/retries.py +77 -66
  60. verifiers/v1/rollout.py +288 -158
  61. verifiers/v1/runtimes/__init__.py +0 -2
  62. verifiers/v1/runtimes/base.py +8 -0
  63. verifiers/v1/runtimes/modal.py +1 -1
  64. verifiers/v1/runtimes/prime.py +1 -1
  65. verifiers/v1/serve/__init__.py +4 -6
  66. verifiers/v1/serve/client.py +13 -10
  67. verifiers/v1/serve/pool.py +8 -7
  68. verifiers/v1/serve/server.py +36 -40
  69. verifiers/v1/serve/types.py +16 -9
  70. verifiers/v1/session.py +27 -0
  71. verifiers/v1/task.py +35 -77
  72. verifiers/v1/taskset.py +21 -30
  73. verifiers/v1/tasksets/__init__.py +0 -2
  74. verifiers/v1/tasksets/harbor/taskset.py +1 -1
  75. verifiers/v1/tasksets/textarena/taskset.py +1 -1
  76. verifiers/v1/trace.py +37 -26
  77. verifiers/v1/utils/__init__.py +0 -1
  78. verifiers/v1/utils/compile.py +108 -0
  79. verifiers/v1/utils/generic.py +35 -1
  80. {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/METADATA +1 -1
  81. {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/RECORD +84 -74
  82. {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/WHEEL +0 -0
  83. {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/entry_points.txt +0 -0
  84. {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/licenses/LICENSE +0 -0
verifiers/v1/__init__.py CHANGED
@@ -1,5 +1,3 @@
1
- """Public v1 API."""
2
-
3
1
  import logging as _logging
4
2
 
5
3
  from pydantic_config import BaseConfig
@@ -8,21 +6,27 @@ from verifiers.v1.clients import (
8
6
  BaseClientConfig,
9
7
  Client,
10
8
  ClientConfig,
9
+ EvalClientConfig,
11
10
  ModelContext,
11
+ TrainClientConfig,
12
12
  resolve_client,
13
13
  )
14
- from verifiers.v1.decorators import group_reward, metric, reward, stop, tool
15
- from verifiers.v1.env import (
14
+ from verifiers.v1.decorators import metric, reward, stop, tool
15
+ from verifiers.v1.agent import Agent, AgentConfig, Agents, make_agent
16
+ from verifiers.v1.configs.env import (
16
17
  ElasticPoolConfig,
17
- EnvConfig,
18
18
  EnvServerConfig,
19
- Environment,
20
19
  StaticPoolConfig,
21
- TimeoutConfig,
22
20
  pool_serve_kwargs,
23
21
  )
24
- from verifiers.v1.episode import Episode
22
+ from verifiers.v1.env import (
23
+ EnvConfig,
24
+ Env,
25
+ default_agent_harness,
26
+ )
27
+ from verifiers.v1.envs.single_agent import SingleAgentEnv, SingleAgentEnvConfig
25
28
  from verifiers.v1.errors import (
29
+ EnvError,
26
30
  HarnessError,
27
31
  InterceptionError,
28
32
  ProviderError,
@@ -51,11 +55,16 @@ from verifiers.v1.judges import (
51
55
  )
52
56
  from verifiers.v1.loaders import (
53
57
  default_harness_id,
58
+ env_config_type,
59
+ resolve_env_config,
60
+ environment_class,
54
61
  harness_config_type,
62
+ import_environment,
55
63
  import_harness,
56
64
  import_judge,
57
65
  import_taskset,
58
66
  judge_config_type,
67
+ load_environment,
59
68
  load_harness,
60
69
  load_judge,
61
70
  load_taskset,
@@ -70,8 +79,7 @@ from verifiers.v1.scoring import (
70
79
  read_answer_file_or_last_reply as read_answer_file_or_last_reply,
71
80
  verify_boxed_math_answer as verify_boxed_math_answer,
72
81
  )
73
- from verifiers.v1.retries import RetryConfig, RolloutRetryConfig
74
- from verifiers.v1.rollout import Rollout
82
+ from verifiers.v1.retries import RetryConfig
75
83
  from verifiers.v1.utils.git import (
76
84
  PATCH_CAP_BYTES as PATCH_CAP_BYTES,
77
85
  capture_patch as capture_patch,
@@ -104,6 +112,7 @@ from verifiers.v1.mcp import (
104
112
  UserConfig,
105
113
  )
106
114
  from verifiers.v1.graph import MessageNode
115
+ from verifiers.v1.episode import Episode, WireEpisode
107
116
  from verifiers.v1.trace import (
108
117
  TRACE_VERSION,
109
118
  AgentInfo,
@@ -176,6 +185,8 @@ __all__ = [
176
185
  "Trace",
177
186
  "TraceTask",
178
187
  "WireTrace",
188
+ "Episode",
189
+ "WireEpisode",
179
190
  "TRACE_VERSION",
180
191
  "AgentInfo",
181
192
  "RunInfo",
@@ -199,9 +210,9 @@ __all__ = [
199
210
  "tool",
200
211
  "metric",
201
212
  "reward",
202
- "group_reward",
203
213
  # errors
204
214
  "RolloutError",
215
+ "EnvError",
205
216
  "ProviderError",
206
217
  "HarnessError",
207
218
  "ToolsetError",
@@ -214,6 +225,8 @@ __all__ = [
214
225
  "Client",
215
226
  "BaseClientConfig",
216
227
  "ClientConfig",
228
+ "EvalClientConfig",
229
+ "TrainClientConfig",
217
230
  "resolve_client",
218
231
  # taskset / harness / runtime / environment
219
232
  "Taskset",
@@ -230,28 +243,37 @@ __all__ = [
230
243
  "SubprocessConfig",
231
244
  "DockerConfig",
232
245
  "PrimeConfig",
233
- "Environment",
246
+ "Env",
247
+ "SingleAgentEnv",
234
248
  "EnvConfig",
235
249
  "EnvServerConfig",
250
+ "SingleAgentEnvConfig",
251
+ "AgentConfig",
236
252
  "StaticPoolConfig",
237
253
  "ElasticPoolConfig",
254
+ "default_agent_harness",
238
255
  "pool_serve_kwargs",
239
256
  "RetryConfig",
240
- "RolloutRetryConfig",
241
- "TimeoutConfig",
242
- "Episode",
243
- "Rollout",
257
+ # agent
258
+ "Agent",
259
+ "Agents",
260
+ "make_agent",
244
261
  # loaders
245
262
  "import_taskset",
246
263
  "import_harness",
247
264
  "import_judge",
265
+ "import_environment",
266
+ "load_environment",
248
267
  "load_taskset",
249
268
  "load_harness",
250
269
  "load_judge",
270
+ "environment_class",
251
271
  "task_type",
252
272
  "taskset_config_type",
253
273
  "harness_config_type",
254
274
  "judge_config_type",
275
+ "env_config_type",
276
+ "resolve_env_config",
255
277
  "default_harness_id",
256
278
  # judge
257
279
  "Judge",
verifiers/v1/agent.py ADDED
@@ -0,0 +1,476 @@
1
+ """The Agent: a reusable (harness x model x runtime) value with one executable
2
+ arrow — `agent.run(task) -> Trace`; `runtime=` borrows a live box,
3
+ `provision(task)` hands you one. Inject a live `Interception` to share servers
4
+ across agents (a pool belongs to what spans agents, never to one agent); an
5
+ entered agent (`async with`) owns one server; un-entered, each run brings its own."""
6
+
7
+ import asyncio
8
+ import logging
9
+ from collections.abc import Callable, Iterator, Mapping
10
+ from contextlib import asynccontextmanager, nullcontext
11
+ from typing import AsyncIterator
12
+
13
+ from pydantic import SerializeAsAny, model_validator
14
+ from pydantic_config import BaseConfig
15
+
16
+ from verifiers.v1.clients import (
17
+ Client,
18
+ ClientConfig,
19
+ EvalClientConfig,
20
+ ModelContext,
21
+ resolve_client,
22
+ )
23
+ from verifiers.v1.harness import HarnessConfig
24
+ from verifiers.v1.interception import Interception, InterceptionServer
25
+ from verifiers.v1.mcp import SharedToolServer
26
+ from verifiers.v1.retries import RetryConfig, backoff, trace_should_retry
27
+ from verifiers.v1.rollout import RolloutRun
28
+ from verifiers.v1.runtimes import (
29
+ Runtime,
30
+ RuntimeConfig,
31
+ SubprocessConfig,
32
+ make_runtime,
33
+ runtime_is_local,
34
+ )
35
+ from verifiers.v1.session import RolloutLimits
36
+ from verifiers.v1.task import Task
37
+ from verifiers.v1.trace import Trace
38
+ from verifiers.v1.types import Sampling, SamplingConfig
39
+ from verifiers.v1.utils.compile import (
40
+ cap_remote_harness_timeout,
41
+ resolve_runtime_config,
42
+ validate_pairing,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class TimeoutConfig(BaseConfig):
49
+ """Per-agent wall-clock timeouts per rollout stage, in seconds (None = no
50
+ limit); each stage falls back to the task's own `TaskTimeout` when unset."""
51
+
52
+ setup: float | None = None # one shared budget: task setup + provisioning
53
+ rollout: float | None = None
54
+ finalize: float | None = None
55
+ scoring: float | None = None
56
+
57
+
58
+ class AgentConfig(BaseConfig):
59
+ """One env agent: who plays it, and its per-run caps. It pins only what
60
+ makes it a different actor; everything unpinned falls back — the model context
61
+ to the run's own, the harness to the taskset's default."""
62
+
63
+ harness: SerializeAsAny[HarnessConfig] | None = None
64
+ """The agent's program + runtime policy (None = the taskset's default harness)."""
65
+ model: str | None = None
66
+ """Model id (None = the run's model, i.e. the policy under evaluation/training)."""
67
+ client: ClientConfig | None = None
68
+ """Endpoint override (None = the run's client)."""
69
+ sampling: SamplingConfig | None = None
70
+ """Sampling override (None = the run's sampling)."""
71
+ timeout: TimeoutConfig = TimeoutConfig()
72
+ retries: RetryConfig = RetryConfig()
73
+ """Whole-run retries: rerun this agent's rollout while its trace ends with a
74
+ retryable error (never into a borrowed box)."""
75
+ max_turns: int | None = None
76
+ """Max model turns per run (None = no limit). Framework-enforced (the
77
+ interception server refuses turns past it), so it applies to any harness."""
78
+ max_input_tokens: int | None = None
79
+ max_output_tokens: int | None = None
80
+ max_total_tokens: int | None = None
81
+ """Token caps per run (None = no limit); framework-enforced between turns."""
82
+
83
+ @model_validator(mode="before")
84
+ @classmethod
85
+ def _resolve_harness(cls, data):
86
+ """Narrow a pinned `harness` to its concrete config type by `id` (absent
87
+ stays None = the taskset's default). The lazy import keeps class-body
88
+ `AgentConfig()` defaults constructible while this module initializes."""
89
+ if isinstance(data, dict) and data.get("harness") is not None:
90
+ from verifiers.v1.loaders import harness_config_type, narrow_plugin_field
91
+
92
+ narrow_plugin_field(data, "harness", harness_config_type, "bash")
93
+ return data
94
+
95
+
96
+ def _check_borrowed_placement(task: Task, runtime: Runtime) -> None:
97
+ """A borrowed box is never re-provisioned, so a task's placement fields can't
98
+ be honored. A task `image` on a subprocess box raises (a wiring bug — it goes
99
+ to the caller, not the trace); a container box whose image differs only warns,
100
+ since placing a run into an existing world is the point of borrowing."""
101
+ if task.data.image is None:
102
+ return
103
+ if isinstance(runtime.config, SubprocessConfig):
104
+ raise ValueError(
105
+ f"task {task.data.idx!r} requires image {task.data.image!r}, but the "
106
+ "borrowed runtime is subprocess-backed (no container); borrow a container "
107
+ "box (e.g. agent.provision(task)) or drop the task's image"
108
+ )
109
+ box_image = getattr(runtime.config, "image", None)
110
+ if box_image != task.data.image:
111
+ logger.warning(
112
+ "task %r requires image %r, but borrowed box %r runs %r; a borrowed box "
113
+ "is never re-provisioned, so the run proceeds in the box's world",
114
+ task.data.idx,
115
+ task.data.image,
116
+ runtime.name,
117
+ box_image,
118
+ )
119
+
120
+
121
+ class Agent:
122
+ """A configured harness + model + runtime policy, runnable on any task.
123
+
124
+ Built from an `AgentConfig` alone; `client=`/`interception=` inject live
125
+ resources to borrow — agents on one endpoint should share one `Client`, and a
126
+ live `Interception`'s owner keeps its lifecycle. The harness config's
127
+ `runtime` is a *policy*: each `run` provisions a fresh box from it, resolved
128
+ per task; `run(runtime=...)` places the run into an existing box instead
129
+ (borrowed boxes are never started or torn down by the run)."""
130
+
131
+ def __init__(
132
+ self,
133
+ config: AgentConfig,
134
+ *,
135
+ client: Client | None = None,
136
+ interception: Interception | None = None,
137
+ ) -> None:
138
+ from verifiers.v1.loaders import harness_config_type, load_harness
139
+
140
+ if config.model is None:
141
+ raise ValueError(
142
+ "AgentConfig.model is unset; an Agent needs a pinned model "
143
+ "(inside an env the run's own model fills it in)"
144
+ )
145
+ harness_config = config.harness
146
+ if harness_config is None:
147
+ harness_config = harness_config_type("bash")(id="bash")
148
+ self.config = config
149
+ self.harness = load_harness(harness_config)
150
+ if client is None:
151
+ client = resolve_client(config.client or EvalClientConfig())
152
+ self.ctx = ModelContext(
153
+ model=config.model,
154
+ client=client,
155
+ sampling=config.sampling if config.sampling is not None else Sampling(),
156
+ )
157
+ self.runtime_config: RuntimeConfig = self.harness.config.runtime
158
+ self.interception = interception
159
+ self.limits = RolloutLimits(
160
+ max_turns=config.max_turns,
161
+ max_input_tokens=config.max_input_tokens,
162
+ max_output_tokens=config.max_output_tokens,
163
+ max_total_tokens=config.max_total_tokens,
164
+ )
165
+ self.timeout = config.timeout
166
+ # Env-owned standing, not config: `Env.setup` marks fixed agents
167
+ # untrainable and traces are stamped from here; inert outside an env.
168
+ self.trainable: bool = True
169
+ self._entered = False
170
+ self._server: InterceptionServer | None = None
171
+ self._warned_resources: set[tuple[str, str]] = set()
172
+
173
+ async def __aenter__(self) -> "Agent":
174
+ if self._entered:
175
+ raise RuntimeError("Agent is already entered; enter it once and share it")
176
+ self._entered = True
177
+ if self.interception is None:
178
+ # Sized to the runtime policy (remote needs the tunnel); runs the
179
+ # server can't serve fall back per run.
180
+ self._server = InterceptionServer(
181
+ requires_tunnel=not runtime_is_local(self.runtime_config)
182
+ )
183
+ try:
184
+ await self._server.__aenter__()
185
+ except BaseException:
186
+ # A failed __aenter__ gets no __aexit__ from `async with`: unwind
187
+ # here, or the agent stays "already entered" forever.
188
+ self._entered, self._server = False, None
189
+ raise
190
+ return self
191
+
192
+ async def __aexit__(self, *exc) -> None:
193
+ self._entered = False
194
+ server, self._server = self._server, None
195
+ if server is not None:
196
+ await server.__aexit__(*exc)
197
+
198
+ def _interception_for(
199
+ self, run_is_local: bool, task: Task, shared_tools: Mapping
200
+ ) -> Interception | None:
201
+ """Which interception this run rides: an injected one always (its owner
202
+ sized its reach); the owned server only when provably reachable from all
203
+ the run's consumers — when it tunnels, else for a local run with no tool
204
+ or user servers in play (such servers may sit in a remote runtime and must
205
+ reach `/state`). Otherwise `None`: a per-run server sized to the task."""
206
+ if self.interception is not None:
207
+ return self.interception
208
+ if self._server is None:
209
+ return None
210
+ if self._server.tunnel is not None or (
211
+ run_is_local
212
+ and not shared_tools
213
+ and not type(task).tools
214
+ and type(task).user is None
215
+ ):
216
+ return self._server
217
+ return None
218
+
219
+ async def run(
220
+ self,
221
+ task: Task,
222
+ *,
223
+ runtime: Runtime | None = None,
224
+ shared_tools: Mapping[str, SharedToolServer] | None = None,
225
+ on_trace: Callable[[Trace], None] | None = None,
226
+ ) -> Trace:
227
+ """Run this agent on `task` once and return the trace: `runtime` places it
228
+ into a live borrowed box instead of provisioning one; `shared_tools` are
229
+ live servers borrowed from their owner, counted in the pairing check;
230
+ `on_trace` observes the trace the moment it's minted, before any I/O.
231
+ Retries whole while the trace ends with a retryable error (`config.retries`)
232
+ — never into a borrowed box; the final trace keeps earlier attempts' errors."""
233
+ retry = self.config.retries
234
+ history: list = []
235
+ for attempt in range(retry.max_retries + 1):
236
+ trace = await self._run_once(task, runtime, shared_tools, on_trace)
237
+ if attempt == retry.max_retries or not trace_should_retry(trace, retry):
238
+ break
239
+ if runtime is not None:
240
+ logger.warning(
241
+ "not retrying the rollout on a borrowed box (its state is no "
242
+ "longer the task's start state); the error stands"
243
+ )
244
+ break
245
+ history.extend(trace.errors)
246
+ delay = backoff(attempt)
247
+ logger.warning(
248
+ "retrying agent rollout (retry %d/%d) in %.1fs after error: %s",
249
+ attempt + 1,
250
+ retry.max_retries,
251
+ delay,
252
+ trace.error.type if trace.error else "?",
253
+ )
254
+ await asyncio.sleep(delay)
255
+ if history:
256
+ # The full history rides the final trace either way; success is the
257
+ # `ok` stamp, never errors-emptiness.
258
+ trace.errors = history + trace.errors
259
+ return trace
260
+
261
+ async def _run_once(
262
+ self,
263
+ task: Task,
264
+ runtime: Runtime | None,
265
+ shared_tools: Mapping[str, SharedToolServer] | None,
266
+ on_trace: Callable[[Trace], None] | None,
267
+ ) -> Trace:
268
+ params = self._rollout_params(task, runtime, dict(shared_tools or {}))
269
+ run = RolloutRun(task=task, on_trace=on_trace, **params)
270
+ try:
271
+ if await run.open():
272
+ await run.step()
273
+ trace = await run.close()
274
+ except BaseException:
275
+ # A cancellation mid-run (or a lifetime bug raised to the caller) means
276
+ # close() never runs — free the run's servers and owned runtime first.
277
+ await run.abort()
278
+ raise
279
+ if trace.runtime is not None:
280
+ trace.runtime.borrowed = runtime is not None
281
+ return trace
282
+
283
+ def _rollout_params(
284
+ self, task: Task, runtime: Runtime | None, shared_tools: dict
285
+ ) -> dict:
286
+ """Resolve one run's runtime config, pairing checks, timeouts, interception."""
287
+ if runtime is not None:
288
+ _check_borrowed_placement(task, runtime)
289
+ runtime_config = runtime.config
290
+ run_is_local = runtime.is_local
291
+ else:
292
+ runtime_config = resolve_runtime_config(
293
+ self.runtime_config, task, self._warned_resources
294
+ )
295
+ run_is_local = runtime_is_local(runtime_config)
296
+ validate_pairing(
297
+ self.harness, type(task), runtime_config, shared_tools=shared_tools
298
+ )
299
+ # Timeout precedence: agent-level wins, else the task's, else no limit.
300
+ harness_timeout = (
301
+ self.timeout.rollout
302
+ if self.timeout.rollout is not None
303
+ else task.data.timeout.harness
304
+ )
305
+ return dict(
306
+ harness=self.harness,
307
+ ctx=self.ctx,
308
+ runtime_config=runtime_config,
309
+ setup_timeout=(
310
+ self.timeout.setup
311
+ if self.timeout.setup is not None
312
+ else task.data.timeout.setup
313
+ ),
314
+ harness_timeout=cap_remote_harness_timeout(
315
+ harness_timeout, runtime_config, task
316
+ ),
317
+ finalize_timeout=(
318
+ self.timeout.finalize
319
+ if self.timeout.finalize is not None
320
+ else task.data.timeout.finalize
321
+ ),
322
+ scoring_timeout=(
323
+ self.timeout.scoring
324
+ if self.timeout.scoring is not None
325
+ else task.data.timeout.scoring
326
+ ),
327
+ limits=self.limits,
328
+ shared_tools=shared_tools,
329
+ interception=self._interception_for(run_is_local, task, shared_tools),
330
+ runtime=runtime,
331
+ )
332
+
333
+ @asynccontextmanager
334
+ async def provision(self, task: Task | None = None) -> AsyncIterator[Runtime]:
335
+ """Provision (and on exit tear down) a box from this agent's runtime
336
+ policy, resolved for `task` when given; share it via `run(..., runtime=box)`."""
337
+ config = (
338
+ resolve_runtime_config(self.runtime_config, task, self._warned_resources)
339
+ if task is not None
340
+ else self.runtime_config
341
+ )
342
+ runtime = make_runtime(config)
343
+ try:
344
+ # start() inside the try: a failed start may already hold a remote
345
+ # sandbox, so it must reach stop() (safe on a partially-started runtime).
346
+ await runtime.start()
347
+ yield runtime
348
+ finally:
349
+ await runtime.stop()
350
+
351
+
352
+ class _EpisodeAgent(Agent):
353
+ """One role's `Agent` for one env-rollout, built fresh per episode (a cheap
354
+ bundle of references — expensive resources are env-owned and borrowed, so no
355
+ state spans concurrent episodes): traces get their agent standing the moment
356
+ they're created, finished ones land in `completed` (the episode's traces),
357
+ each run takes the eval's gate. The taskset's shared tool servers ride only
358
+ its own tasks — on an env-minted task they'd wrongly put MCP in play
359
+ (`shared_tools=` overrides)."""
360
+
361
+ def __init__(
362
+ self,
363
+ config: AgentConfig,
364
+ *,
365
+ client: Client,
366
+ interception: Interception | None,
367
+ name: str,
368
+ shared_tools: Mapping[str, SharedToolServer],
369
+ task_cls: type[Task],
370
+ gate: asyncio.Semaphore | None,
371
+ completed: list[Trace],
372
+ on_trace: Callable[[Trace], None] | None,
373
+ on_discard: Callable[[Trace], None] | None,
374
+ warned_resources: set,
375
+ ) -> None:
376
+ super().__init__(config, client=client, interception=interception)
377
+ # Resource warnings dedupe env-wide, not per episode.
378
+ self._warned_resources = warned_resources
379
+ self._name = name
380
+ self._shared_tools = shared_tools
381
+ self._task_cls = task_cls
382
+ self._gate = gate
383
+ self._completed = completed
384
+ self._on_trace = on_trace
385
+ self._on_discard = on_discard
386
+
387
+ def _shared_for(self, task: Task) -> Mapping[str, SharedToolServer]:
388
+ return self._shared_tools if isinstance(task, self._task_cls) else {}
389
+
390
+ async def run(
391
+ self,
392
+ task: Task,
393
+ *,
394
+ runtime: Runtime | None = None,
395
+ shared_tools: Mapping[str, SharedToolServer] | None = None,
396
+ on_trace: Callable[[Trace], None] | None = None,
397
+ ) -> Trace:
398
+ last: Trace | None = None
399
+
400
+ def watch(trace: Trace) -> None:
401
+ nonlocal last
402
+ if trace.agent is not None:
403
+ trace.agent.name = self._name
404
+ trace.agent.trainable = self.trainable
405
+ # A per-agent retry mints a replacement: the abandoned attempt's trace
406
+ # must leave live views (only the final one joins the episode).
407
+ if last is not None and self._on_discard is not None:
408
+ self._on_discard(last)
409
+ last = trace
410
+ if self._on_trace is not None:
411
+ self._on_trace(trace)
412
+ if on_trace is not None:
413
+ on_trace(trace)
414
+
415
+ async with self._gate or nullcontext():
416
+ trace = await super().run(
417
+ task,
418
+ runtime=runtime,
419
+ shared_tools=shared_tools
420
+ if shared_tools is not None
421
+ else self._shared_for(task),
422
+ on_trace=watch,
423
+ )
424
+ self._completed.append(trace)
425
+ return trace
426
+
427
+
428
+ def make_agent(
429
+ config: AgentConfig,
430
+ *,
431
+ client: Client | None = None,
432
+ interception: Interception | None = None,
433
+ ) -> Agent:
434
+ """The agent for a config; `client`/`interception` inject live resources to
435
+ borrow, everything else comes from the config."""
436
+ return Agent(config, client=client, interception=interception)
437
+
438
+
439
+ MakeAgent = Callable[[str, AgentConfig], Agent]
440
+ """An agent factory keyed by name — what `Agents` calls per scraped config field."""
441
+
442
+
443
+ def agent_config_fields(config) -> dict[str, AgentConfig]:
444
+ """The top-level `AgentConfig` fields declared on a config, in declaration
445
+ order — the env's agents, keyed by field name (the only naming site)."""
446
+ return {name: value for name, value in config if isinstance(value, AgentConfig)}
447
+
448
+
449
+ class Agents:
450
+ """A config's agents, addressed by attribute: every top-level `AgentConfig`
451
+ field becomes an `Agent` under the field's name (`agents.solver`)."""
452
+
453
+ def __init__(self, config, make: MakeAgent | None = None) -> None:
454
+ if make is None:
455
+ make = lambda _, spec: make_agent(spec) # noqa: E731
456
+ self._agents: dict[str, Agent] = {
457
+ name: make(name, value)
458
+ for name, value in agent_config_fields(config).items()
459
+ }
460
+
461
+ def __getattr__(self, name: str) -> Agent:
462
+ # self.__dict__ directly: attribute lookup re-entering __getattr__ before
463
+ # __init__ ran (copy/unpickle) must raise, not recurse.
464
+ agents = self.__dict__.get("_agents")
465
+ if agents is None or name not in agents:
466
+ raise AttributeError(
467
+ f"no agent {name!r}; this config declares "
468
+ f"{sorted(agents) if agents else []}"
469
+ )
470
+ return agents[name]
471
+
472
+ def __iter__(self) -> Iterator[Agent]:
473
+ return iter(self._agents.values())
474
+
475
+ def __len__(self) -> int:
476
+ return len(self._agents)
@@ -1 +0,0 @@
1
- """V1 CLI commands."""
@@ -1,5 +1,3 @@
1
- """Rich live dashboards."""
2
-
3
1
  from verifiers.v1.cli.dashboard.eval import dashboard
4
2
  from verifiers.v1.cli.dashboard.validate import TaskProgress, validate_dashboard
5
3