verifiers 0.2.2.dev18__py3-none-any.whl → 0.2.2.dev19__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 +13 -8
- verifiers/v1/acp/__init__.py +64 -0
- verifiers/v1/acp/_runner.py +192 -0
- verifiers/v1/agent.py +297 -29
- verifiers/v1/cli/init.py +10 -59
- verifiers/v1/configs/init.py +0 -2
- verifiers/v1/dialects/base.py +1 -14
- verifiers/v1/dialects/chat.py +3 -15
- verifiers/v1/dialects/responses.py +0 -72
- verifiers/v1/env.py +3 -3
- verifiers/v1/envs/user_sim/__init__.py +3 -0
- verifiers/v1/envs/user_sim/env.py +95 -0
- verifiers/v1/errors.py +3 -7
- verifiers/v1/harness.py +87 -17
- verifiers/v1/harnesses/bash/harness.py +4 -3
- verifiers/v1/harnesses/claude_code/harness.py +4 -2
- verifiers/v1/harnesses/codex/harness.py +109 -22
- verifiers/v1/harnesses/kimi_code/harness.py +42 -12
- verifiers/v1/harnesses/mini_swe_agent/harness.py +3 -1
- verifiers/v1/harnesses/null/harness.py +4 -3
- verifiers/v1/harnesses/pi/harness.py +89 -117
- verifiers/v1/harnesses/pool/harness.py +29 -60
- verifiers/v1/harnesses/rlm/harness.py +17 -11
- verifiers/v1/harnesses/terminus_2/harness.py +4 -4
- verifiers/v1/interception/__init__.py +7 -4
- verifiers/v1/interception/base.py +2 -2
- verifiers/v1/interception/server.py +143 -196
- verifiers/v1/interception/tunnel/custom.py +1 -1
- verifiers/v1/mcp/__init__.py +0 -9
- verifiers/v1/mcp/launch.py +15 -131
- verifiers/v1/rollout.py +114 -51
- verifiers/v1/runtimes/subprocess.py +40 -4
- verifiers/v1/session.py +2 -15
- verifiers/v1/state.py +1 -1
- verifiers/v1/task.py +10 -13
- verifiers/v1/tasksets/__init__.py +4 -8
- verifiers/v1/tasksets/openenv/__init__.py +4 -8
- verifiers/v1/tasksets/openenv/taskset.py +95 -95
- verifiers/v1/tasksets/textarena/__init__.py +4 -6
- verifiers/v1/tasksets/textarena/taskset.py +45 -80
- verifiers/v1/utils/compile.py +2 -8
- {verifiers-0.2.2.dev18.dist-info → verifiers-0.2.2.dev19.dist-info}/METADATA +1 -1
- {verifiers-0.2.2.dev18.dist-info → verifiers-0.2.2.dev19.dist-info}/RECORD +46 -43
- verifiers/v1/mcp/user.py +0 -76
- {verifiers-0.2.2.dev18.dist-info → verifiers-0.2.2.dev19.dist-info}/WHEEL +0 -0
- {verifiers-0.2.2.dev18.dist-info → verifiers-0.2.2.dev19.dist-info}/entry_points.txt +0 -0
- {verifiers-0.2.2.dev18.dist-info → verifiers-0.2.2.dev19.dist-info}/licenses/LICENSE +0 -0
verifiers/v1/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging as _logging
|
|
|
2
2
|
|
|
3
3
|
from pydantic_config import BaseConfig
|
|
4
4
|
|
|
5
|
+
from verifiers.v1.acp import ACP
|
|
5
6
|
from verifiers.v1.clients import (
|
|
6
7
|
BaseClientConfig,
|
|
7
8
|
Client,
|
|
@@ -12,7 +13,14 @@ from verifiers.v1.clients import (
|
|
|
12
13
|
resolve_client,
|
|
13
14
|
)
|
|
14
15
|
from verifiers.v1.decorators import metric, reward, stop, tool
|
|
15
|
-
from verifiers.v1.agent import
|
|
16
|
+
from verifiers.v1.agent import (
|
|
17
|
+
Agent,
|
|
18
|
+
AgentConfig,
|
|
19
|
+
Agents,
|
|
20
|
+
Interaction,
|
|
21
|
+
Segment,
|
|
22
|
+
make_agent,
|
|
23
|
+
)
|
|
16
24
|
from verifiers.v1.configs.env import (
|
|
17
25
|
ElasticPoolConfig,
|
|
18
26
|
EnvServerConfig,
|
|
@@ -35,7 +43,6 @@ from verifiers.v1.errors import (
|
|
|
35
43
|
TaskError,
|
|
36
44
|
ToolsetError,
|
|
37
45
|
TunnelError,
|
|
38
|
-
UserError,
|
|
39
46
|
)
|
|
40
47
|
from verifiers.v1.harness import Harness, HarnessConfig
|
|
41
48
|
from verifiers.v1.judge import (
|
|
@@ -108,8 +115,6 @@ from verifiers.v1.mcp import (
|
|
|
108
115
|
Toolset,
|
|
109
116
|
SharedToolsetConfig,
|
|
110
117
|
ToolsetConfig,
|
|
111
|
-
User,
|
|
112
|
-
UserConfig,
|
|
113
118
|
)
|
|
114
119
|
from verifiers.v1.graph import MessageNode
|
|
115
120
|
from verifiers.v1.episode import Episode, WireEpisode
|
|
@@ -216,7 +221,6 @@ __all__ = [
|
|
|
216
221
|
"ProviderError",
|
|
217
222
|
"HarnessError",
|
|
218
223
|
"ToolsetError",
|
|
219
|
-
"UserError",
|
|
220
224
|
"SandboxError",
|
|
221
225
|
"TaskError",
|
|
222
226
|
"InterceptionError",
|
|
@@ -235,6 +239,7 @@ __all__ = [
|
|
|
235
239
|
"BaseConfig",
|
|
236
240
|
"Harness",
|
|
237
241
|
"HarnessConfig",
|
|
242
|
+
"ACP",
|
|
238
243
|
"ModelContext",
|
|
239
244
|
"Runtime",
|
|
240
245
|
"RuntimeConfig",
|
|
@@ -302,9 +307,9 @@ __all__ = [
|
|
|
302
307
|
"Toolset",
|
|
303
308
|
"SharedToolsetConfig",
|
|
304
309
|
"ToolsetConfig",
|
|
305
|
-
# user
|
|
306
|
-
"
|
|
307
|
-
"
|
|
310
|
+
# the user channel
|
|
311
|
+
"Interaction",
|
|
312
|
+
"Segment",
|
|
308
313
|
]
|
|
309
314
|
|
|
310
315
|
# The library logs via stdlib logging (per-module `getLogger(__name__)`), but is
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Public Agent Client Protocol support for harness programs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import secrets
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from verifiers.v1.dialects.chat import message_to_wire
|
|
8
|
+
from verifiers.v1.harness import Harness
|
|
9
|
+
from verifiers.v1.runtimes import ProgramResult, Runtime
|
|
10
|
+
from verifiers.v1.types import Messages
|
|
11
|
+
from verifiers.v1.utils.aio import run_shielded
|
|
12
|
+
|
|
13
|
+
ACP_SOURCE = (Path(__file__).resolve().parent / "_runner.py").read_text()
|
|
14
|
+
|
|
15
|
+
__all__ = ["ACP"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ACP:
|
|
19
|
+
"""Run an ACP agent."""
|
|
20
|
+
|
|
21
|
+
async def setup(self, harness: Harness, runtime: Runtime) -> None:
|
|
22
|
+
await runtime.prepare_uv_script(
|
|
23
|
+
ACP_SOURCE, {**harness.config.resolved_env, "UV_FROZEN": "false"}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def run(
|
|
27
|
+
self,
|
|
28
|
+
runtime: Runtime,
|
|
29
|
+
env: dict[str, str],
|
|
30
|
+
command: list[str],
|
|
31
|
+
prompt: str | Messages | None,
|
|
32
|
+
*,
|
|
33
|
+
mcp_urls: dict[str, str] | None = None,
|
|
34
|
+
system_prompt: str | None = None,
|
|
35
|
+
session_path: str | None = None,
|
|
36
|
+
) -> ProgramResult:
|
|
37
|
+
if prompt is None:
|
|
38
|
+
raise ValueError("ACP requires a prompt")
|
|
39
|
+
messages = (
|
|
40
|
+
[{"role": "user", "content": prompt}]
|
|
41
|
+
if isinstance(prompt, str)
|
|
42
|
+
else [message_to_wire(message) for message in prompt]
|
|
43
|
+
)
|
|
44
|
+
config = {
|
|
45
|
+
"command": command,
|
|
46
|
+
"messages": messages,
|
|
47
|
+
"mcp_urls": mcp_urls or {},
|
|
48
|
+
"system_prompt": system_prompt or "",
|
|
49
|
+
"session_path": session_path,
|
|
50
|
+
}
|
|
51
|
+
program = await runtime.prepare_uv_script(
|
|
52
|
+
ACP_SOURCE, {**env, "UV_FROZEN": "false"}
|
|
53
|
+
)
|
|
54
|
+
directory = f".vf-acp-{secrets.token_hex(8)}"
|
|
55
|
+
created = await runtime.run(["mkdir", "-m", "700", directory], {})
|
|
56
|
+
if created.exit_code != 0:
|
|
57
|
+
raise RuntimeError(f"ACP config directory failed: {created.stderr.strip()}")
|
|
58
|
+
path = f"{directory}/config.json"
|
|
59
|
+
try:
|
|
60
|
+
await runtime.write(path, json.dumps(config).encode())
|
|
61
|
+
result = await runtime.run_program([*program, path], env)
|
|
62
|
+
return result
|
|
63
|
+
finally:
|
|
64
|
+
await run_shielded(runtime.run(["rm", "-rf", directory], {}))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10,<3.15"
|
|
3
|
+
# dependencies = ["agent-client-protocol==0.11.0"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Run one harness segment through an ACP agent."""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from acp import (
|
|
15
|
+
PROTOCOL_VERSION,
|
|
16
|
+
Client,
|
|
17
|
+
RequestError,
|
|
18
|
+
image_block,
|
|
19
|
+
spawn_agent_process,
|
|
20
|
+
text_block,
|
|
21
|
+
)
|
|
22
|
+
from acp.schema import (
|
|
23
|
+
AgentMessageChunk,
|
|
24
|
+
AllowedOutcome,
|
|
25
|
+
ClientCapabilities,
|
|
26
|
+
DeniedOutcome,
|
|
27
|
+
HttpMcpServer,
|
|
28
|
+
PermissionOption,
|
|
29
|
+
RequestPermissionResponse,
|
|
30
|
+
TextContentBlock,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VerifiersClient(Client):
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self.visible_reply = ""
|
|
37
|
+
self.message_id: str | None = None
|
|
38
|
+
|
|
39
|
+
async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None:
|
|
40
|
+
if not isinstance(update, AgentMessageChunk) or not isinstance(
|
|
41
|
+
update.content, TextContentBlock
|
|
42
|
+
):
|
|
43
|
+
return
|
|
44
|
+
message_id = getattr(update, "message_id", None)
|
|
45
|
+
if message_id is not None and message_id != self.message_id:
|
|
46
|
+
self.visible_reply = ""
|
|
47
|
+
self.message_id = message_id
|
|
48
|
+
self.visible_reply += update.content.text
|
|
49
|
+
|
|
50
|
+
async def request_permission(
|
|
51
|
+
self,
|
|
52
|
+
session_id: str,
|
|
53
|
+
tool_call: Any,
|
|
54
|
+
options: list[PermissionOption],
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> RequestPermissionResponse:
|
|
57
|
+
option = next(
|
|
58
|
+
(item for item in options if item.kind in ("allow_once", "allow_always")),
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
outcome = (
|
|
62
|
+
AllowedOutcome(outcome="selected", option_id=option.option_id)
|
|
63
|
+
if option
|
|
64
|
+
else DeniedOutcome(outcome="cancelled")
|
|
65
|
+
)
|
|
66
|
+
return RequestPermissionResponse(outcome=outcome)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def content_blocks(messages: list[dict], supports_images: bool) -> list:
|
|
70
|
+
blocks = []
|
|
71
|
+
transcript = len(messages) != 1 or messages[0].get("role") != "user"
|
|
72
|
+
for message in messages:
|
|
73
|
+
if transcript:
|
|
74
|
+
separator = "\n\n" if blocks else ""
|
|
75
|
+
blocks.append(
|
|
76
|
+
text_block(f"{separator}[{message.get('role', 'message')}]\n")
|
|
77
|
+
)
|
|
78
|
+
content = message.get("content") or ""
|
|
79
|
+
parts = (
|
|
80
|
+
[{"type": "text", "text": content}] if isinstance(content, str) else content
|
|
81
|
+
)
|
|
82
|
+
for part in parts:
|
|
83
|
+
if part["type"] == "text":
|
|
84
|
+
blocks.append(text_block(part["text"]))
|
|
85
|
+
continue
|
|
86
|
+
if not supports_images:
|
|
87
|
+
raise ValueError("ACP agent does not support image prompts")
|
|
88
|
+
url = part["image_url"]["url"]
|
|
89
|
+
metadata, separator, data = url.partition(",")
|
|
90
|
+
media_type, *parameters = metadata.removeprefix("data:").split(";")
|
|
91
|
+
if (
|
|
92
|
+
not separator
|
|
93
|
+
or not metadata.startswith("data:image/")
|
|
94
|
+
or not any(value.lower() == "base64" for value in parameters)
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("ACP image prompts require base64 data:image URLs")
|
|
97
|
+
blocks.append(image_block(data, media_type))
|
|
98
|
+
metadata = {
|
|
99
|
+
key: value
|
|
100
|
+
for key, value in message.items()
|
|
101
|
+
if key not in ("role", "content") and value
|
|
102
|
+
}
|
|
103
|
+
if metadata:
|
|
104
|
+
blocks.append(text_block("\n" + json.dumps(metadata, ensure_ascii=False)))
|
|
105
|
+
return blocks
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def run_client(config: dict) -> None:
|
|
109
|
+
client = VerifiersClient()
|
|
110
|
+
command = config["command"]
|
|
111
|
+
async with spawn_agent_process(
|
|
112
|
+
client,
|
|
113
|
+
command[0],
|
|
114
|
+
*command[1:],
|
|
115
|
+
env=os.environ.copy(),
|
|
116
|
+
transport_kwargs={"stderr": None},
|
|
117
|
+
) as (connection, _process):
|
|
118
|
+
initialized = await connection.initialize(
|
|
119
|
+
protocol_version=PROTOCOL_VERSION,
|
|
120
|
+
client_capabilities=ClientCapabilities(),
|
|
121
|
+
)
|
|
122
|
+
capabilities = initialized.agent_capabilities
|
|
123
|
+
prompt_capabilities = capabilities and capabilities.prompt_capabilities
|
|
124
|
+
supports_images = bool(prompt_capabilities and prompt_capabilities.image)
|
|
125
|
+
mcp_servers = [
|
|
126
|
+
HttpMcpServer(type="http", name=name, url=url, headers=[])
|
|
127
|
+
for name, url in config["mcp_urls"].items()
|
|
128
|
+
]
|
|
129
|
+
session_path = Path(config["session_path"]) if config["session_path"] else None
|
|
130
|
+
is_new = session_path is None or not session_path.exists()
|
|
131
|
+
if is_new:
|
|
132
|
+
session = await connection.new_session(
|
|
133
|
+
cwd=os.getcwd(), mcp_servers=mcp_servers
|
|
134
|
+
)
|
|
135
|
+
session_id = session.session_id
|
|
136
|
+
else:
|
|
137
|
+
session_id = session_path.read_text().strip()
|
|
138
|
+
session_capabilities = capabilities and capabilities.session_capabilities
|
|
139
|
+
if session_capabilities and session_capabilities.resume is not None:
|
|
140
|
+
await connection.resume_session(
|
|
141
|
+
cwd=os.getcwd(), session_id=session_id, mcp_servers=mcp_servers
|
|
142
|
+
)
|
|
143
|
+
elif capabilities and capabilities.load_session:
|
|
144
|
+
await connection.load_session(
|
|
145
|
+
cwd=os.getcwd(), session_id=session_id, mcp_servers=mcp_servers
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
raise RuntimeError("ACP agent does not support resuming sessions")
|
|
149
|
+
|
|
150
|
+
messages = config["messages"]
|
|
151
|
+
if not is_new:
|
|
152
|
+
last_assistant = max(
|
|
153
|
+
(
|
|
154
|
+
index
|
|
155
|
+
for index, message in enumerate(messages)
|
|
156
|
+
if message.get("role") == "assistant"
|
|
157
|
+
),
|
|
158
|
+
default=-1,
|
|
159
|
+
)
|
|
160
|
+
messages = messages[last_assistant + 1 :]
|
|
161
|
+
if is_new and config["system_prompt"]:
|
|
162
|
+
messages = [
|
|
163
|
+
{"role": "system", "content": config["system_prompt"]},
|
|
164
|
+
*messages,
|
|
165
|
+
]
|
|
166
|
+
prompt = content_blocks(messages, supports_images)
|
|
167
|
+
if not prompt:
|
|
168
|
+
raise ValueError("ACP prompt has no content")
|
|
169
|
+
client.visible_reply = ""
|
|
170
|
+
client.message_id = None
|
|
171
|
+
try:
|
|
172
|
+
await connection.prompt(session_id=session_id, prompt=prompt)
|
|
173
|
+
except RequestError as error:
|
|
174
|
+
detail = error.data.get("details") if isinstance(error.data, dict) else None
|
|
175
|
+
raise RuntimeError(detail or str(error)) from error
|
|
176
|
+
if not client.visible_reply.strip():
|
|
177
|
+
raise RuntimeError("ACP agent produced no visible reply")
|
|
178
|
+
sys.stdout.write(client.visible_reply)
|
|
179
|
+
if session_path and is_new:
|
|
180
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
session_path.write_text(session_id)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def main() -> None:
|
|
185
|
+
path = Path(sys.argv[1])
|
|
186
|
+
config = json.loads(path.read_text())
|
|
187
|
+
path.unlink()
|
|
188
|
+
await run_client(config)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
asyncio.run(main())
|