verifiers 0.2.2.dev17__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.
Files changed (47) hide show
  1. verifiers/v1/__init__.py +13 -8
  2. verifiers/v1/acp/__init__.py +64 -0
  3. verifiers/v1/acp/_runner.py +192 -0
  4. verifiers/v1/agent.py +297 -29
  5. verifiers/v1/cli/init.py +10 -59
  6. verifiers/v1/configs/init.py +0 -2
  7. verifiers/v1/dialects/base.py +1 -14
  8. verifiers/v1/dialects/chat.py +3 -15
  9. verifiers/v1/dialects/responses.py +0 -72
  10. verifiers/v1/env.py +3 -3
  11. verifiers/v1/envs/user_sim/__init__.py +3 -0
  12. verifiers/v1/envs/user_sim/env.py +95 -0
  13. verifiers/v1/errors.py +3 -7
  14. verifiers/v1/harness.py +123 -17
  15. verifiers/v1/harnesses/bash/harness.py +5 -3
  16. verifiers/v1/harnesses/claude_code/harness.py +8 -3
  17. verifiers/v1/harnesses/codex/harness.py +112 -22
  18. verifiers/v1/harnesses/kimi_code/harness.py +45 -12
  19. verifiers/v1/harnesses/mini_swe_agent/harness.py +3 -1
  20. verifiers/v1/harnesses/null/harness.py +5 -3
  21. verifiers/v1/harnesses/pi/harness.py +101 -117
  22. verifiers/v1/harnesses/pool/harness.py +32 -60
  23. verifiers/v1/harnesses/rlm/harness.py +26 -16
  24. verifiers/v1/harnesses/terminus_2/harness.py +6 -14
  25. verifiers/v1/interception/__init__.py +7 -4
  26. verifiers/v1/interception/base.py +2 -2
  27. verifiers/v1/interception/server.py +143 -196
  28. verifiers/v1/interception/tunnel/custom.py +1 -1
  29. verifiers/v1/mcp/__init__.py +0 -9
  30. verifiers/v1/mcp/launch.py +15 -131
  31. verifiers/v1/rollout.py +114 -51
  32. verifiers/v1/runtimes/subprocess.py +40 -4
  33. verifiers/v1/session.py +2 -15
  34. verifiers/v1/state.py +1 -1
  35. verifiers/v1/task.py +10 -13
  36. verifiers/v1/tasksets/__init__.py +4 -8
  37. verifiers/v1/tasksets/openenv/__init__.py +4 -8
  38. verifiers/v1/tasksets/openenv/taskset.py +95 -95
  39. verifiers/v1/tasksets/textarena/__init__.py +4 -6
  40. verifiers/v1/tasksets/textarena/taskset.py +45 -80
  41. verifiers/v1/utils/compile.py +12 -6
  42. {verifiers-0.2.2.dev17.dist-info → verifiers-0.2.2.dev19.dist-info}/METADATA +1 -1
  43. {verifiers-0.2.2.dev17.dist-info → verifiers-0.2.2.dev19.dist-info}/RECORD +46 -43
  44. verifiers/v1/mcp/user.py +0 -76
  45. {verifiers-0.2.2.dev17.dist-info → verifiers-0.2.2.dev19.dist-info}/WHEEL +0 -0
  46. {verifiers-0.2.2.dev17.dist-info → verifiers-0.2.2.dev19.dist-info}/entry_points.txt +0 -0
  47. {verifiers-0.2.2.dev17.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 Agent, AgentConfig, Agents, make_agent
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 simulator
306
- "User",
307
- "UserConfig",
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())