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.
- verifiers/v1/__init__.py +38 -16
- verifiers/v1/agent.py +476 -0
- verifiers/v1/cli/__init__.py +0 -1
- verifiers/v1/cli/dashboard/__init__.py +0 -2
- verifiers/v1/cli/dashboard/eval.py +298 -153
- verifiers/v1/cli/debug.py +11 -6
- verifiers/v1/cli/eval/__init__.py +0 -1
- verifiers/v1/cli/eval/main.py +32 -18
- verifiers/v1/cli/eval/resume.py +66 -47
- verifiers/v1/cli/eval/runner.py +64 -66
- verifiers/v1/cli/gepa.py +36 -16
- verifiers/v1/cli/init.py +3 -3
- verifiers/v1/cli/output.py +80 -36
- verifiers/v1/cli/replay.py +55 -41
- verifiers/v1/cli/resolve.py +77 -46
- verifiers/v1/cli/serve.py +17 -9
- verifiers/v1/cli/validate.py +14 -6
- verifiers/v1/clients/__init__.py +0 -2
- verifiers/v1/clients/client.py +2 -2
- verifiers/v1/configs/__init__.py +0 -2
- verifiers/v1/configs/env.py +169 -0
- verifiers/v1/configs/eval.py +14 -6
- verifiers/v1/configs/init.py +1 -1
- verifiers/v1/configs/replay.py +12 -5
- verifiers/v1/configs/serve.py +1 -1
- verifiers/v1/decorators.py +7 -16
- verifiers/v1/dialects/__init__.py +1 -4
- verifiers/v1/dialects/base.py +0 -21
- verifiers/v1/env.py +450 -323
- verifiers/v1/envs/__init__.py +0 -0
- verifiers/v1/envs/agentic_judge/__init__.py +3 -0
- verifiers/v1/envs/agentic_judge/env.py +174 -0
- verifiers/v1/envs/best_of_n/__init__.py +3 -0
- verifiers/v1/envs/best_of_n/env.py +44 -0
- verifiers/v1/envs/single_agent/__init__.py +3 -0
- verifiers/v1/envs/single_agent/env.py +27 -0
- verifiers/v1/episode.py +53 -55
- verifiers/v1/errors.py +8 -2
- verifiers/v1/gepa/__init__.py +0 -7
- verifiers/v1/gepa/adapter.py +57 -40
- verifiers/v1/gepa/config.py +23 -10
- verifiers/v1/gepa/dataset.py +0 -15
- verifiers/v1/gepa/runner.py +8 -12
- verifiers/v1/graph.py +0 -4
- verifiers/v1/harness.py +22 -15
- verifiers/v1/harnesses/bash/harness.py +3 -3
- verifiers/v1/harnesses/null/harness.py +1 -0
- verifiers/v1/harnesses/terminus_2/harness.py +1 -1
- verifiers/v1/interception/__init__.py +1 -11
- verifiers/v1/interception/server.py +35 -4
- verifiers/v1/interception/tunnel/__init__.py +0 -9
- verifiers/v1/judge.py +9 -1
- verifiers/v1/judges/__init__.py +0 -2
- verifiers/v1/legacy.py +27 -15
- verifiers/v1/loaders.py +111 -8
- verifiers/v1/mcp/__init__.py +0 -2
- verifiers/v1/mcp/launch.py +4 -5
- verifiers/v1/push.py +81 -55
- verifiers/v1/retries.py +77 -66
- verifiers/v1/rollout.py +288 -158
- verifiers/v1/runtimes/__init__.py +0 -2
- verifiers/v1/runtimes/base.py +8 -0
- verifiers/v1/runtimes/modal.py +1 -1
- verifiers/v1/runtimes/prime.py +1 -1
- verifiers/v1/serve/__init__.py +4 -6
- verifiers/v1/serve/client.py +13 -10
- verifiers/v1/serve/pool.py +8 -7
- verifiers/v1/serve/server.py +36 -40
- verifiers/v1/serve/types.py +16 -9
- verifiers/v1/session.py +27 -0
- verifiers/v1/task.py +35 -77
- verifiers/v1/taskset.py +21 -30
- verifiers/v1/tasksets/__init__.py +0 -2
- verifiers/v1/tasksets/harbor/taskset.py +1 -1
- verifiers/v1/tasksets/textarena/taskset.py +1 -1
- verifiers/v1/trace.py +37 -26
- verifiers/v1/utils/__init__.py +0 -1
- verifiers/v1/utils/compile.py +108 -0
- verifiers/v1/utils/generic.py +35 -1
- {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/METADATA +1 -1
- {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/RECORD +84 -74
- {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/WHEEL +0 -0
- {verifiers-0.2.2.dev6.dist-info → verifiers-0.2.2.dev7.dist-info}/entry_points.txt +0 -0
- {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
|
|
15
|
-
from verifiers.v1.
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
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)
|
verifiers/v1/cli/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""V1 CLI commands."""
|