roller-agent 0.2.0__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.
- roller/__init__.py +31 -0
- roller/__main__.py +6 -0
- roller/actf.py +431 -0
- roller/artifacts.py +124 -0
- roller/assets/deepseek/journal.mjs +44 -0
- roller/assets/deepseek/roller.patch.yml +15 -0
- roller/assets/deepseek/runner.py +149 -0
- roller/assets/hermes/runner.py +322 -0
- roller/assets/openclaw_capture.py +32 -0
- roller/assets/openclaw_drain.mjs +21 -0
- roller/assets/opencode/journal.mjs +42 -0
- roller/assets/opencode/runner.py +203 -0
- roller/assets/pi/runner.mjs +160 -0
- roller/assets/pi/runner.py +93 -0
- roller/benches/__init__.py +73 -0
- roller/benches/base.py +306 -0
- roller/benches/grading.py +553 -0
- roller/benches/pinchbench.py +976 -0
- roller/benches/qwenclaw/__init__.py +49 -0
- roller/benches/qwenclaw/adapter.py +167 -0
- roller/benches/qwenclaw/grader_runtime.py +395 -0
- roller/benches/qwenclaw/report.py +49 -0
- roller/benches/qwenclaw/task_parser.py +181 -0
- roller/benches/qwenclaw/workspace.py +202 -0
- roller/benches/report.py +166 -0
- roller/benches/skillsbench.py +708 -0
- roller/benches/transcripts.py +102 -0
- roller/benches/wildclaw.py +569 -0
- roller/bridge_capture.py +73 -0
- roller/bundles.py +545 -0
- roller/capture.py +385 -0
- roller/capture_protocol.py +301 -0
- roller/capture_runtime.py +48 -0
- roller/capture_states.py +37 -0
- roller/claude_capture.py +185 -0
- roller/claude_code_actf.py +594 -0
- roller/cli.py +1732 -0
- roller/codex_actf.py +584 -0
- roller/codex_capture.py +99 -0
- roller/codex_tools.py +137 -0
- roller/collector.py +818 -0
- roller/compaction_sft.py +672 -0
- roller/conversation.py +432 -0
- roller/deepseek_actf.py +135 -0
- roller/harness_registry.py +61 -0
- roller/harnesses/__init__.py +29 -0
- roller/harnesses/base.py +64 -0
- roller/harnesses/bridge.py +448 -0
- roller/harnesses/claude_code.py +839 -0
- roller/harnesses/codex.py +808 -0
- roller/harnesses/config_overrides.py +100 -0
- roller/harnesses/container.py +206 -0
- roller/harnesses/deepseek.py +104 -0
- roller/harnesses/docker.py +72 -0
- roller/harnesses/gateway_ports.py +155 -0
- roller/harnesses/hermes.py +119 -0
- roller/harnesses/openclaw.py +1605 -0
- roller/harnesses/opencode.py +193 -0
- roller/harnesses/pi.py +111 -0
- roller/hermes_actf.py +147 -0
- roller/image_build.py +206 -0
- roller/integrity.py +332 -0
- roller/model_protocols.py +201 -0
- roller/models.py +169 -0
- roller/openclaw_capture.py +140 -0
- roller/openclaw_delivery.py +211 -0
- roller/openclaw_usage.py +59 -0
- roller/opencode_actf.py +305 -0
- roller/opencode_capture.py +96 -0
- roller/pi_actf.py +172 -0
- roller/response_metadata.py +89 -0
- roller/run_lock.py +297 -0
- roller/sft.py +471 -0
- roller/storage.py +53 -0
- roller/tasks.py +689 -0
- roller/thinking.py +50 -0
- roller/thinking_capabilities.py +152 -0
- roller/trajectory_audit.py +386 -0
- roller/usage.py +168 -0
- roller/version.py +1 -0
- roller/viewer/__init__.py +3 -0
- roller/viewer/app.py +722 -0
- roller/viewer/static/app.js +752 -0
- roller/viewer/static/index.html +105 -0
- roller/viewer/static/style.css +213 -0
- roller/wire_trace.py +650 -0
- roller_agent-0.2.0.dist-info/METADATA +397 -0
- roller_agent-0.2.0.dist-info/RECORD +92 -0
- roller_agent-0.2.0.dist-info/WHEEL +5 -0
- roller_agent-0.2.0.dist-info/entry_points.txt +2 -0
- roller_agent-0.2.0.dist-info/licenses/LICENSE +201 -0
- roller_agent-0.2.0.dist-info/top_level.txt +1 -0
roller/benches/base.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
BENCH_SCHEMA = "roller.bench/v1"
|
|
11
|
+
DEFAULT_GRADE_TIMEOUT = 120
|
|
12
|
+
MAX_GRADE_TIMEOUT = 86_400
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BenchFormatError(ValueError):
|
|
16
|
+
"""Raised when a bench task list or grade block cannot be understood."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class GradeSpec:
|
|
21
|
+
"""A scorer that runs inside the sample's own container before teardown.
|
|
22
|
+
|
|
23
|
+
Benches ship their grader as Python source rather than as a host-side
|
|
24
|
+
callable: the score usually depends on the container filesystem the agent
|
|
25
|
+
just wrote to, which no longer exists once the harness tears it down.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
code: str
|
|
29
|
+
entrypoint: str = "grade"
|
|
30
|
+
kwargs: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
env_names: tuple[str, ...] = ()
|
|
32
|
+
optional_env_names: tuple[str, ...] = ()
|
|
33
|
+
timeout_seconds: int = DEFAULT_GRADE_TIMEOUT
|
|
34
|
+
needs_transcript: bool = True
|
|
35
|
+
workspace_path: str | None = None
|
|
36
|
+
transcript_path: str | None = None
|
|
37
|
+
score_key: str = "overall_score"
|
|
38
|
+
# Host paths copied into the container immediately before grading, keyed by
|
|
39
|
+
# their destination relative to the grading workspace. Answer keys live here
|
|
40
|
+
# rather than in the task's workspace: anything the agent can read it can
|
|
41
|
+
# also copy from, and a bench whose expected output ships alongside its seed
|
|
42
|
+
# files is not measuring the model.
|
|
43
|
+
stage: tuple[tuple[str, str], ...] = ()
|
|
44
|
+
|
|
45
|
+
def as_dict(self) -> dict[str, Any]:
|
|
46
|
+
payload: dict[str, Any] = {"code": self.code}
|
|
47
|
+
if self.entrypoint != "grade":
|
|
48
|
+
payload["entrypoint"] = self.entrypoint
|
|
49
|
+
if self.kwargs:
|
|
50
|
+
payload["kwargs"] = dict(self.kwargs)
|
|
51
|
+
if self.env_names:
|
|
52
|
+
payload["env"] = list(self.env_names)
|
|
53
|
+
if self.optional_env_names:
|
|
54
|
+
payload["optional_env"] = list(self.optional_env_names)
|
|
55
|
+
if self.timeout_seconds != DEFAULT_GRADE_TIMEOUT:
|
|
56
|
+
payload["timeout_seconds"] = self.timeout_seconds
|
|
57
|
+
if not self.needs_transcript:
|
|
58
|
+
payload["needs_transcript"] = False
|
|
59
|
+
if self.workspace_path is not None:
|
|
60
|
+
payload["workspace_path"] = self.workspace_path
|
|
61
|
+
if self.transcript_path is not None:
|
|
62
|
+
payload["transcript_path"] = self.transcript_path
|
|
63
|
+
if self.score_key != "overall_score":
|
|
64
|
+
payload["score_key"] = self.score_key
|
|
65
|
+
if self.stage:
|
|
66
|
+
payload["stage"] = [
|
|
67
|
+
{"source": source, "target": target} for source, target in self.stage
|
|
68
|
+
]
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, payload: Any, label: str = "bench.grade") -> "GradeSpec":
|
|
73
|
+
if not isinstance(payload, dict):
|
|
74
|
+
raise BenchFormatError(f"{label}: must be an object")
|
|
75
|
+
code = payload.get("code")
|
|
76
|
+
if not isinstance(code, str) or not code.strip():
|
|
77
|
+
raise BenchFormatError(f"{label}.code: must be non-empty Python source")
|
|
78
|
+
entrypoint = payload.get("entrypoint", "grade")
|
|
79
|
+
if not isinstance(entrypoint, str) or not entrypoint.isidentifier():
|
|
80
|
+
raise BenchFormatError(f"{label}.entrypoint: must be a Python identifier")
|
|
81
|
+
kwargs = payload.get("kwargs", {})
|
|
82
|
+
if not isinstance(kwargs, dict):
|
|
83
|
+
raise BenchFormatError(f"{label}.kwargs: must be an object")
|
|
84
|
+
env_names = _env_names(payload.get("env"), f"{label}.env")
|
|
85
|
+
optional_env_names = _env_names(
|
|
86
|
+
payload.get("optional_env"), f"{label}.optional_env"
|
|
87
|
+
)
|
|
88
|
+
timeout_seconds = payload.get("timeout_seconds", DEFAULT_GRADE_TIMEOUT)
|
|
89
|
+
if (
|
|
90
|
+
isinstance(timeout_seconds, bool)
|
|
91
|
+
or not isinstance(timeout_seconds, int)
|
|
92
|
+
or not 1 <= timeout_seconds <= MAX_GRADE_TIMEOUT
|
|
93
|
+
):
|
|
94
|
+
raise BenchFormatError(
|
|
95
|
+
f"{label}.timeout_seconds: must be an integer from 1 to {MAX_GRADE_TIMEOUT}"
|
|
96
|
+
)
|
|
97
|
+
needs_transcript = payload.get("needs_transcript", True)
|
|
98
|
+
if not isinstance(needs_transcript, bool):
|
|
99
|
+
raise BenchFormatError(f"{label}.needs_transcript: must be boolean")
|
|
100
|
+
workspace_path = payload.get("workspace_path")
|
|
101
|
+
if workspace_path is not None and (
|
|
102
|
+
not isinstance(workspace_path, str) or not workspace_path.startswith("/")
|
|
103
|
+
):
|
|
104
|
+
raise BenchFormatError(
|
|
105
|
+
f"{label}.workspace_path: must be an absolute container path"
|
|
106
|
+
)
|
|
107
|
+
transcript_path = payload.get("transcript_path")
|
|
108
|
+
if transcript_path is not None and (
|
|
109
|
+
not isinstance(transcript_path, str) or not transcript_path.startswith("/")
|
|
110
|
+
):
|
|
111
|
+
raise BenchFormatError(
|
|
112
|
+
f"{label}.transcript_path: must be an absolute container path"
|
|
113
|
+
)
|
|
114
|
+
score_key = payload.get("score_key", "overall_score")
|
|
115
|
+
if not isinstance(score_key, str) or not score_key.strip():
|
|
116
|
+
raise BenchFormatError(f"{label}.score_key: must be a non-empty string")
|
|
117
|
+
return cls(
|
|
118
|
+
code=code,
|
|
119
|
+
entrypoint=entrypoint,
|
|
120
|
+
kwargs=kwargs,
|
|
121
|
+
env_names=env_names,
|
|
122
|
+
optional_env_names=optional_env_names,
|
|
123
|
+
timeout_seconds=timeout_seconds,
|
|
124
|
+
needs_transcript=needs_transcript,
|
|
125
|
+
workspace_path=workspace_path,
|
|
126
|
+
transcript_path=transcript_path,
|
|
127
|
+
score_key=score_key.strip(),
|
|
128
|
+
stage=_stage_files(payload.get("stage"), f"{label}.stage"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _stage_files(value: Any, label: str) -> tuple[tuple[str, str], ...]:
|
|
133
|
+
"""Parse ``stage`` entries into ``(host source, workspace-relative target)``."""
|
|
134
|
+
|
|
135
|
+
if value is None:
|
|
136
|
+
return ()
|
|
137
|
+
if not isinstance(value, list):
|
|
138
|
+
raise BenchFormatError(f"{label}: must be a list")
|
|
139
|
+
entries: list[tuple[str, str]] = []
|
|
140
|
+
for index, item in enumerate(value):
|
|
141
|
+
entry_label = f"{label}[{index}]"
|
|
142
|
+
if not isinstance(item, dict):
|
|
143
|
+
raise BenchFormatError(f"{entry_label}: must be an object")
|
|
144
|
+
source = item.get("source")
|
|
145
|
+
if not isinstance(source, str) or not source.startswith("/"):
|
|
146
|
+
raise BenchFormatError(f"{entry_label}.source: must be an absolute host path")
|
|
147
|
+
target = item.get("target")
|
|
148
|
+
if not isinstance(target, str) or not target.strip():
|
|
149
|
+
raise BenchFormatError(f"{entry_label}.target: must be a non-empty string")
|
|
150
|
+
target = target.strip()
|
|
151
|
+
parts = PurePosixPath(target).parts
|
|
152
|
+
# A target that escapes the workspace would let a bench definition write
|
|
153
|
+
# anywhere in the container, so reject rather than normalize it away.
|
|
154
|
+
if target.startswith("/") or not parts or any(part == ".." for part in parts):
|
|
155
|
+
raise BenchFormatError(
|
|
156
|
+
f"{entry_label}.target: must be a relative path inside the workspace"
|
|
157
|
+
)
|
|
158
|
+
entries.append((source, target))
|
|
159
|
+
return tuple(entries)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _env_names(value: Any, label: str) -> tuple[str, ...]:
|
|
163
|
+
if value is None or value == "":
|
|
164
|
+
return ()
|
|
165
|
+
if isinstance(value, str):
|
|
166
|
+
names = [line.strip() for line in value.splitlines()]
|
|
167
|
+
elif isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
168
|
+
names = [item.strip() for item in value]
|
|
169
|
+
else:
|
|
170
|
+
raise BenchFormatError(f"{label}: must be a string or string list")
|
|
171
|
+
return tuple(name for name in names if name and not name.startswith("#"))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class BenchBinding:
|
|
176
|
+
"""The ``bench`` block carried by one exported task row."""
|
|
177
|
+
|
|
178
|
+
name: str
|
|
179
|
+
grade: GradeSpec | None = None
|
|
180
|
+
category: str | None = None
|
|
181
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
182
|
+
|
|
183
|
+
def as_dict(self) -> dict[str, Any]:
|
|
184
|
+
payload: dict[str, Any] = {"schema_version": BENCH_SCHEMA, "name": self.name}
|
|
185
|
+
if self.category:
|
|
186
|
+
payload["category"] = self.category
|
|
187
|
+
if self.metadata:
|
|
188
|
+
payload["metadata"] = dict(self.metadata)
|
|
189
|
+
if self.grade is not None:
|
|
190
|
+
payload["grade"] = self.grade.as_dict()
|
|
191
|
+
return payload
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_dict(cls, payload: Any, label: str = "bench") -> "BenchBinding":
|
|
195
|
+
if not isinstance(payload, dict):
|
|
196
|
+
raise BenchFormatError(f"{label}: must be an object")
|
|
197
|
+
schema_version = payload.get("schema_version", BENCH_SCHEMA)
|
|
198
|
+
if schema_version != BENCH_SCHEMA:
|
|
199
|
+
raise BenchFormatError(
|
|
200
|
+
f"{label}.schema_version: unsupported {schema_version!r}; "
|
|
201
|
+
f"expected {BENCH_SCHEMA!r}"
|
|
202
|
+
)
|
|
203
|
+
name = payload.get("name")
|
|
204
|
+
if not isinstance(name, str) or not name.strip():
|
|
205
|
+
raise BenchFormatError(f"{label}.name: must be a non-empty string")
|
|
206
|
+
category = payload.get("category")
|
|
207
|
+
if category is not None and not isinstance(category, str):
|
|
208
|
+
raise BenchFormatError(f"{label}.category: must be a string")
|
|
209
|
+
metadata = payload.get("metadata", {})
|
|
210
|
+
if not isinstance(metadata, dict):
|
|
211
|
+
raise BenchFormatError(f"{label}.metadata: must be an object")
|
|
212
|
+
grade_payload = payload.get("grade")
|
|
213
|
+
grade = (
|
|
214
|
+
GradeSpec.from_dict(grade_payload, f"{label}.grade")
|
|
215
|
+
if grade_payload is not None
|
|
216
|
+
else None
|
|
217
|
+
)
|
|
218
|
+
return cls(
|
|
219
|
+
name=name.strip(),
|
|
220
|
+
grade=grade,
|
|
221
|
+
category=category,
|
|
222
|
+
metadata=metadata,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def binding_from_task_row(row: dict[str, Any]) -> BenchBinding | None:
|
|
227
|
+
"""Read the optional ``bench`` block from a ``roller.task/v1`` row."""
|
|
228
|
+
|
|
229
|
+
payload = row.get("bench") if isinstance(row, dict) else None
|
|
230
|
+
if payload is None:
|
|
231
|
+
return None
|
|
232
|
+
return BenchBinding.from_dict(payload)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class BenchAdapter(ABC):
|
|
236
|
+
"""Turn an upstream benchmark into ``roller.task/v1`` rows plus scorers.
|
|
237
|
+
|
|
238
|
+
Adapters never execute anything. They translate a checked-out benchmark
|
|
239
|
+
into task rows that Roller's existing collect path already understands,
|
|
240
|
+
embedding each task's scorer in its ``bench.grade`` block so grading
|
|
241
|
+
travels with the task instead of needing a parallel lookup at runtime.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
name: str
|
|
245
|
+
description: str = ""
|
|
246
|
+
variants: tuple[str, ...] = ()
|
|
247
|
+
default_variant: str | None = None
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
def load(
|
|
251
|
+
self,
|
|
252
|
+
root: Path,
|
|
253
|
+
*,
|
|
254
|
+
categories: Sequence[str] | None = None,
|
|
255
|
+
task_ids: Sequence[str] | None = None,
|
|
256
|
+
variant: str | None = None,
|
|
257
|
+
timeout_multiplier: float = 1.0,
|
|
258
|
+
) -> list[dict[str, Any]]:
|
|
259
|
+
"""Return ``roller.task/v1`` rows, each with a ``bench`` block."""
|
|
260
|
+
|
|
261
|
+
def categories(self, root: Path) -> list[str]:
|
|
262
|
+
"""List the selectable category names, if the bench has any."""
|
|
263
|
+
|
|
264
|
+
seen: dict[str, None] = {}
|
|
265
|
+
for row in self.load(root):
|
|
266
|
+
binding = binding_from_task_row(row)
|
|
267
|
+
if binding is not None and binding.category:
|
|
268
|
+
seen.setdefault(binding.category, None)
|
|
269
|
+
return list(seen)
|
|
270
|
+
|
|
271
|
+
def wrap_harness(
|
|
272
|
+
self,
|
|
273
|
+
harness: Any,
|
|
274
|
+
binding: BenchBinding,
|
|
275
|
+
*,
|
|
276
|
+
grading_semaphore: Any = None,
|
|
277
|
+
) -> Any:
|
|
278
|
+
"""Attach this bench's live-container grader to a harness."""
|
|
279
|
+
|
|
280
|
+
if binding.grade is None:
|
|
281
|
+
return harness
|
|
282
|
+
from roller.benches.grading import GradedHarness
|
|
283
|
+
|
|
284
|
+
return GradedHarness(
|
|
285
|
+
harness,
|
|
286
|
+
binding,
|
|
287
|
+
grading_semaphore=grading_semaphore,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def aggregate(self, samples: Iterable[dict[str, Any]]) -> dict[str, Any]:
|
|
291
|
+
"""Summarize per-sample scores; override to match upstream reporting."""
|
|
292
|
+
|
|
293
|
+
from roller.benches.report import mean_aggregate
|
|
294
|
+
|
|
295
|
+
return mean_aggregate(samples)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def validate_timeout_multiplier(value: float) -> float:
|
|
299
|
+
"""Validate the common export-time timeout scale used by bench adapters."""
|
|
300
|
+
|
|
301
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
302
|
+
raise BenchFormatError("timeout multiplier must be a number")
|
|
303
|
+
multiplier = float(value)
|
|
304
|
+
if not math.isfinite(multiplier) or multiplier <= 0:
|
|
305
|
+
raise BenchFormatError("timeout multiplier must be finite and positive")
|
|
306
|
+
return multiplier
|