verifiers 0.2.2.dev88__py3-none-any.whl → 0.2.2.dev90__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 +4 -1
- verifiers/v1/acp/__init__.py +252 -6
- verifiers/v1/acp/runner.py +427 -0
- verifiers/v1/harness.py +106 -15
- verifiers/v1/harnesses/claude_code/harness.py +121 -61
- verifiers/v1/harnesses/codex/harness.py +152 -238
- verifiers/v1/harnesses/node.py +54 -0
- verifiers/v1/harnesses/pi/harness.py +6 -31
- verifiers/v1/harnesses/rlm/harness.py +74 -29
- verifiers/v1/interception/server.py +1 -1
- verifiers/v1/mcp/launch.py +22 -9
- verifiers/v1/rollout.py +33 -11
- verifiers/v1/runtimes/__init__.py +2 -0
- verifiers/v1/runtimes/base.py +37 -0
- verifiers/v1/runtimes/docker/__init__.py +162 -1
- verifiers/v1/runtimes/modal.py +99 -0
- verifiers/v1/runtimes/prime.py +44 -0
- verifiers/v1/runtimes/subprocess.py +67 -13
- verifiers/v1/session.py +1 -1
- {verifiers-0.2.2.dev88.dist-info → verifiers-0.2.2.dev90.dist-info}/METADATA +2 -2
- {verifiers-0.2.2.dev88.dist-info → verifiers-0.2.2.dev90.dist-info}/RECORD +24 -23
- verifiers/v1/acp/_runner.py +0 -213
- {verifiers-0.2.2.dev88.dist-info → verifiers-0.2.2.dev90.dist-info}/WHEEL +0 -0
- {verifiers-0.2.2.dev88.dist-info → verifiers-0.2.2.dev90.dist-info}/entry_points.txt +0 -0
- {verifiers-0.2.2.dev88.dist-info → verifiers-0.2.2.dev90.dist-info}/licenses/LICENSE +0 -0
verifiers/v1/__init__.py
CHANGED
|
@@ -43,7 +43,7 @@ from verifiers.v1.errors import (
|
|
|
43
43
|
TunnelError,
|
|
44
44
|
)
|
|
45
45
|
from verifiers.v1.graph import MessageNode
|
|
46
|
-
from verifiers.v1.harness import Harness
|
|
46
|
+
from verifiers.v1.harness import Harness, HarnessSession
|
|
47
47
|
from verifiers.v1.judge import Judge, JudgeResponse, JudgeView
|
|
48
48
|
from verifiers.v1.judges import (
|
|
49
49
|
Criterion,
|
|
@@ -64,6 +64,7 @@ from verifiers.v1.runtimes import (
|
|
|
64
64
|
Runtime,
|
|
65
65
|
RuntimeConfig,
|
|
66
66
|
RuntimeInfo,
|
|
67
|
+
RuntimeProcess,
|
|
67
68
|
SubprocessConfig,
|
|
68
69
|
)
|
|
69
70
|
from verifiers.v1.state import State, StateT
|
|
@@ -241,10 +242,12 @@ __all__ = [ # noqa: RUF022 - grouped by public API area
|
|
|
241
242
|
"TasksetConfig",
|
|
242
243
|
"BaseConfig",
|
|
243
244
|
"Harness",
|
|
245
|
+
"HarnessSession",
|
|
244
246
|
"HarnessConfig",
|
|
245
247
|
"ACP",
|
|
246
248
|
"ModelContext",
|
|
247
249
|
"Runtime",
|
|
250
|
+
"RuntimeProcess",
|
|
248
251
|
"RuntimeConfig",
|
|
249
252
|
"RuntimeInfo",
|
|
250
253
|
"ProgramResult",
|
verifiers/v1/acp/__init__.py
CHANGED
|
@@ -1,28 +1,70 @@
|
|
|
1
1
|
"""Public Agent Client Protocol support for harness programs."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
3
5
|
import json
|
|
4
6
|
import secrets
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
|
|
10
|
+
from verifiers.v1.clients import ModelContext
|
|
7
11
|
from verifiers.v1.dialects.chat import message_to_wire
|
|
8
|
-
from verifiers.v1.
|
|
9
|
-
from verifiers.v1.
|
|
12
|
+
from verifiers.v1.errors import HarnessError
|
|
13
|
+
from verifiers.v1.harness import Harness, HarnessSession
|
|
14
|
+
from verifiers.v1.runtimes import ProgramResult, Runtime, RuntimeProcess
|
|
15
|
+
from verifiers.v1.task import TaskData
|
|
16
|
+
from verifiers.v1.trace import Trace
|
|
10
17
|
from verifiers.v1.types import Messages
|
|
11
18
|
from verifiers.v1.utils.aio import run_shielded
|
|
12
19
|
|
|
13
|
-
ACP_SOURCE = (Path(__file__).resolve().parent / "
|
|
20
|
+
ACP_SOURCE = (Path(__file__).resolve().parent / "runner.py").read_text()
|
|
21
|
+
MAX_PACKET_BYTES = 128 * 1024 * 1024
|
|
14
22
|
|
|
15
23
|
__all__ = ["ACP"]
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class ACP:
|
|
19
|
-
"""Run
|
|
27
|
+
"""Run one-shot ACP agents or create rollout-scoped ACP sessions."""
|
|
20
28
|
|
|
21
29
|
async def setup(self, harness: Harness, runtime: Runtime) -> None:
|
|
22
30
|
await runtime.prepare_uv_script(
|
|
23
31
|
ACP_SOURCE, {**harness.config.resolved_env, "UV_FROZEN": "false"}
|
|
24
32
|
)
|
|
25
33
|
|
|
34
|
+
def session(
|
|
35
|
+
self,
|
|
36
|
+
harness: Harness,
|
|
37
|
+
ctx: ModelContext,
|
|
38
|
+
trace: Trace,
|
|
39
|
+
runtime: Runtime,
|
|
40
|
+
endpoint: str,
|
|
41
|
+
secret: str,
|
|
42
|
+
mcp_urls: dict[str, str],
|
|
43
|
+
data: TaskData,
|
|
44
|
+
*,
|
|
45
|
+
env: dict[str, str],
|
|
46
|
+
command: list[str],
|
|
47
|
+
prompt: str | Messages | None,
|
|
48
|
+
system_prompt: str | None = None,
|
|
49
|
+
session_meta: dict | None = None,
|
|
50
|
+
) -> "ACPHarnessSession":
|
|
51
|
+
"""Create a persistent ACP-backed handle owned by one rollout."""
|
|
52
|
+
return ACPHarnessSession(
|
|
53
|
+
harness,
|
|
54
|
+
ctx,
|
|
55
|
+
trace,
|
|
56
|
+
runtime,
|
|
57
|
+
endpoint,
|
|
58
|
+
secret,
|
|
59
|
+
mcp_urls,
|
|
60
|
+
data,
|
|
61
|
+
env=env,
|
|
62
|
+
command=command,
|
|
63
|
+
prompt=prompt,
|
|
64
|
+
system_prompt=system_prompt,
|
|
65
|
+
session_meta=session_meta,
|
|
66
|
+
)
|
|
67
|
+
|
|
26
68
|
async def run(
|
|
27
69
|
self,
|
|
28
70
|
runtime: Runtime,
|
|
@@ -33,6 +75,33 @@ class ACP:
|
|
|
33
75
|
mcp_urls: dict[str, str] | None = None,
|
|
34
76
|
system_prompt: str | None = None,
|
|
35
77
|
session_path: str | None = None,
|
|
78
|
+
session_meta: dict | None = None,
|
|
79
|
+
allow_empty_tool_reply: bool = False,
|
|
80
|
+
) -> ProgramResult:
|
|
81
|
+
"""Run one ACP segment without retaining its process."""
|
|
82
|
+
return await self._run(
|
|
83
|
+
runtime,
|
|
84
|
+
env,
|
|
85
|
+
command,
|
|
86
|
+
prompt,
|
|
87
|
+
mcp_urls=mcp_urls,
|
|
88
|
+
system_prompt=system_prompt,
|
|
89
|
+
session_path=session_path,
|
|
90
|
+
session_meta=session_meta,
|
|
91
|
+
allow_empty_tool_reply=allow_empty_tool_reply,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def _run(
|
|
95
|
+
self,
|
|
96
|
+
runtime: Runtime,
|
|
97
|
+
env: dict[str, str],
|
|
98
|
+
command: list[str],
|
|
99
|
+
prompt: str | Messages | None,
|
|
100
|
+
*,
|
|
101
|
+
mcp_urls: dict[str, str] | None = None,
|
|
102
|
+
system_prompt: str | None = None,
|
|
103
|
+
session_path: str | None = None,
|
|
104
|
+
session_meta: dict | None = None,
|
|
36
105
|
allow_empty_tool_reply: bool = False,
|
|
37
106
|
) -> ProgramResult:
|
|
38
107
|
if prompt is None:
|
|
@@ -48,6 +117,7 @@ class ACP:
|
|
|
48
117
|
"mcp_urls": mcp_urls or {},
|
|
49
118
|
"system_prompt": system_prompt or "",
|
|
50
119
|
"session_path": session_path,
|
|
120
|
+
"session_meta": session_meta or {},
|
|
51
121
|
"allow_empty_tool_reply": allow_empty_tool_reply,
|
|
52
122
|
}
|
|
53
123
|
program = await runtime.prepare_uv_script(
|
|
@@ -60,7 +130,183 @@ class ACP:
|
|
|
60
130
|
path = f"{directory}/config.json"
|
|
61
131
|
try:
|
|
62
132
|
await runtime.write(path, json.dumps(config).encode())
|
|
63
|
-
|
|
64
|
-
return result
|
|
133
|
+
return await runtime.run_program([*program, "once", path], env)
|
|
65
134
|
finally:
|
|
66
135
|
await run_shielded(runtime.run(["rm", "-rf", directory], {}))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _packet(value: dict) -> bytes:
|
|
139
|
+
data = json.dumps(value, ensure_ascii=False).encode()
|
|
140
|
+
if len(data) > MAX_PACKET_BYTES:
|
|
141
|
+
raise ValueError(f"ACP session packet is too large: {len(data)} bytes")
|
|
142
|
+
return len(data).to_bytes(8, "big") + data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _PacketReader:
|
|
146
|
+
def __init__(self, source: AsyncIterator[bytes]) -> None:
|
|
147
|
+
self._source = source.__aiter__()
|
|
148
|
+
self._buffer = bytearray()
|
|
149
|
+
|
|
150
|
+
async def _readexactly(self, size: int) -> bytes:
|
|
151
|
+
while len(self._buffer) < size:
|
|
152
|
+
try:
|
|
153
|
+
self._buffer.extend(await anext(self._source))
|
|
154
|
+
except StopAsyncIteration as e:
|
|
155
|
+
raise EOFError("ACP process closed its stdout") from e
|
|
156
|
+
data = bytes(self._buffer[:size])
|
|
157
|
+
del self._buffer[:size]
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
async def read(self) -> dict:
|
|
161
|
+
size = int.from_bytes(await self._readexactly(8), "big")
|
|
162
|
+
if size > MAX_PACKET_BYTES:
|
|
163
|
+
raise ValueError(f"ACP session packet is too large: {size} bytes")
|
|
164
|
+
return json.loads((await self._readexactly(size)).decode())
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ACPHarnessSession(HarnessSession):
|
|
168
|
+
"""A live ACP process, connection, and native session for one rollout."""
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
harness: Harness,
|
|
173
|
+
ctx: ModelContext,
|
|
174
|
+
trace: Trace,
|
|
175
|
+
runtime: Runtime,
|
|
176
|
+
endpoint: str,
|
|
177
|
+
secret: str,
|
|
178
|
+
mcp_urls: dict[str, str],
|
|
179
|
+
data: TaskData,
|
|
180
|
+
env: dict[str, str],
|
|
181
|
+
command: list[str],
|
|
182
|
+
prompt: str | Messages | None,
|
|
183
|
+
system_prompt: str | None,
|
|
184
|
+
session_meta: dict | None,
|
|
185
|
+
) -> None:
|
|
186
|
+
super().__init__(harness, ctx, trace, runtime, endpoint, secret, mcp_urls, data)
|
|
187
|
+
self.env = env
|
|
188
|
+
self.command = command
|
|
189
|
+
self.prompt = prompt
|
|
190
|
+
self.system_prompt = system_prompt
|
|
191
|
+
self.session_meta = session_meta or {}
|
|
192
|
+
self._process: RuntimeProcess | None = None
|
|
193
|
+
self._reader: _PacketReader | None = None
|
|
194
|
+
self._stderr_tail = bytearray()
|
|
195
|
+
self._stderr_task: asyncio.Task[None] | None = None
|
|
196
|
+
self._lock = asyncio.Lock()
|
|
197
|
+
|
|
198
|
+
async def _start(self) -> None:
|
|
199
|
+
self._stderr_tail.clear()
|
|
200
|
+
program = await self.runtime.prepare_uv_script(
|
|
201
|
+
ACP_SOURCE, {**self.env, "UV_FROZEN": "false"}
|
|
202
|
+
)
|
|
203
|
+
process = await self.runtime.open_process([*program, "stream"], self.env)
|
|
204
|
+
self._process = process
|
|
205
|
+
self._reader = _PacketReader(process.stdout)
|
|
206
|
+
self._stderr_task = asyncio.create_task(self._drain_stderr(process.stderr))
|
|
207
|
+
|
|
208
|
+
async def _drain_stderr(self, stream: AsyncIterator[bytes]) -> None:
|
|
209
|
+
async for chunk in stream:
|
|
210
|
+
self._stderr_tail.extend(chunk)
|
|
211
|
+
if len(self._stderr_tail) > 4000:
|
|
212
|
+
del self._stderr_tail[:-4000]
|
|
213
|
+
|
|
214
|
+
def _stderr(self) -> str:
|
|
215
|
+
return self._stderr_tail.decode(errors="replace").strip()
|
|
216
|
+
|
|
217
|
+
async def _run(self, messages: Messages | None) -> ProgramResult:
|
|
218
|
+
prompt = self.prompt if messages is None else messages
|
|
219
|
+
if prompt is None:
|
|
220
|
+
raise ValueError("ACP requires a prompt")
|
|
221
|
+
wire_messages = (
|
|
222
|
+
[{"role": "user", "content": prompt}]
|
|
223
|
+
if isinstance(prompt, str)
|
|
224
|
+
else [message_to_wire(message) for message in prompt]
|
|
225
|
+
)
|
|
226
|
+
config = {
|
|
227
|
+
"command": self.command,
|
|
228
|
+
"messages": wire_messages,
|
|
229
|
+
"mcp_urls": self.mcp_urls,
|
|
230
|
+
"system_prompt": self.system_prompt or "",
|
|
231
|
+
"session_path": None,
|
|
232
|
+
"session_meta": self.session_meta,
|
|
233
|
+
}
|
|
234
|
+
async with self._lock:
|
|
235
|
+
if self._closed:
|
|
236
|
+
raise HarnessError(
|
|
237
|
+
f"harness {self.harness.config.id!r} session is already closed"
|
|
238
|
+
)
|
|
239
|
+
if self._process is None:
|
|
240
|
+
await self._start()
|
|
241
|
+
assert self._process is not None
|
|
242
|
+
assert self._reader is not None
|
|
243
|
+
try:
|
|
244
|
+
await self._process.write(
|
|
245
|
+
_packet({"operation": "prompt", "config": config})
|
|
246
|
+
)
|
|
247
|
+
response = await self._reader.read()
|
|
248
|
+
except BaseException:
|
|
249
|
+
await run_shielded(self._stop(graceful=False))
|
|
250
|
+
raise
|
|
251
|
+
if not response.get("ok"):
|
|
252
|
+
detail = response.get("error") or "ACP session request failed"
|
|
253
|
+
if stderr := self._stderr():
|
|
254
|
+
detail = f"{detail}\n\nACP process stderr:\n{stderr}"
|
|
255
|
+
raise RuntimeError(detail)
|
|
256
|
+
return ProgramResult(exit_code=0, stdout=response.get("reply", ""), stderr="")
|
|
257
|
+
|
|
258
|
+
async def _stop(self, *, graceful: bool) -> None:
|
|
259
|
+
process, self._process = self._process, None
|
|
260
|
+
reader, self._reader = self._reader, None
|
|
261
|
+
stderr_task, self._stderr_task = self._stderr_task, None
|
|
262
|
+
if process is None:
|
|
263
|
+
return
|
|
264
|
+
failure: BaseException | None = None
|
|
265
|
+
try:
|
|
266
|
+
if graceful and reader is not None:
|
|
267
|
+
try:
|
|
268
|
+
await process.write(_packet({"operation": "shutdown"}))
|
|
269
|
+
response = await asyncio.wait_for(reader.read(), timeout=10)
|
|
270
|
+
if not response.get("ok"):
|
|
271
|
+
raise RuntimeError(
|
|
272
|
+
response.get("error") or "ACP session shutdown failed"
|
|
273
|
+
)
|
|
274
|
+
except BaseException as error: # noqa: BLE001 - finish teardown if cancelled
|
|
275
|
+
failure = error
|
|
276
|
+
try:
|
|
277
|
+
await asyncio.wait_for(process.wait(), timeout=10 if graceful else 0.1)
|
|
278
|
+
except BaseException: # noqa: BLE001 - cancellation still requires termination
|
|
279
|
+
with contextlib.suppress(Exception):
|
|
280
|
+
await asyncio.wait_for(process.terminate(), timeout=5)
|
|
281
|
+
try:
|
|
282
|
+
await asyncio.wait_for(process.wait(), timeout=5)
|
|
283
|
+
except BaseException: # noqa: BLE001 - cancellation still requires a kill
|
|
284
|
+
with contextlib.suppress(Exception):
|
|
285
|
+
await asyncio.wait_for(process.kill(), timeout=5)
|
|
286
|
+
with contextlib.suppress(BaseException):
|
|
287
|
+
await asyncio.wait_for(process.wait(), timeout=5)
|
|
288
|
+
finally:
|
|
289
|
+
if stderr_task is not None:
|
|
290
|
+
if not stderr_task.done():
|
|
291
|
+
stderr_task.cancel()
|
|
292
|
+
with contextlib.suppress(BaseException):
|
|
293
|
+
await stderr_task
|
|
294
|
+
if failure is not None:
|
|
295
|
+
detail = str(failure)
|
|
296
|
+
if stderr := self._stderr():
|
|
297
|
+
detail = f"{detail}\n\nACP process stderr:\n{stderr}"
|
|
298
|
+
raise RuntimeError(detail) from failure
|
|
299
|
+
|
|
300
|
+
async def close(self) -> None:
|
|
301
|
+
if self._closed:
|
|
302
|
+
return
|
|
303
|
+
# Publish closure before waiting for the process lock. A turn that
|
|
304
|
+
# already passed HarnessSession.turn()'s fast check rechecks under the
|
|
305
|
+
# same lock in _run(), so it cannot restart after teardown.
|
|
306
|
+
await super().close()
|
|
307
|
+
|
|
308
|
+
async def close_process() -> None:
|
|
309
|
+
async with self._lock:
|
|
310
|
+
await self._stop(graceful=True)
|
|
311
|
+
|
|
312
|
+
await run_shielded(close_process())
|