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.
Files changed (92) hide show
  1. roller/__init__.py +31 -0
  2. roller/__main__.py +6 -0
  3. roller/actf.py +431 -0
  4. roller/artifacts.py +124 -0
  5. roller/assets/deepseek/journal.mjs +44 -0
  6. roller/assets/deepseek/roller.patch.yml +15 -0
  7. roller/assets/deepseek/runner.py +149 -0
  8. roller/assets/hermes/runner.py +322 -0
  9. roller/assets/openclaw_capture.py +32 -0
  10. roller/assets/openclaw_drain.mjs +21 -0
  11. roller/assets/opencode/journal.mjs +42 -0
  12. roller/assets/opencode/runner.py +203 -0
  13. roller/assets/pi/runner.mjs +160 -0
  14. roller/assets/pi/runner.py +93 -0
  15. roller/benches/__init__.py +73 -0
  16. roller/benches/base.py +306 -0
  17. roller/benches/grading.py +553 -0
  18. roller/benches/pinchbench.py +976 -0
  19. roller/benches/qwenclaw/__init__.py +49 -0
  20. roller/benches/qwenclaw/adapter.py +167 -0
  21. roller/benches/qwenclaw/grader_runtime.py +395 -0
  22. roller/benches/qwenclaw/report.py +49 -0
  23. roller/benches/qwenclaw/task_parser.py +181 -0
  24. roller/benches/qwenclaw/workspace.py +202 -0
  25. roller/benches/report.py +166 -0
  26. roller/benches/skillsbench.py +708 -0
  27. roller/benches/transcripts.py +102 -0
  28. roller/benches/wildclaw.py +569 -0
  29. roller/bridge_capture.py +73 -0
  30. roller/bundles.py +545 -0
  31. roller/capture.py +385 -0
  32. roller/capture_protocol.py +301 -0
  33. roller/capture_runtime.py +48 -0
  34. roller/capture_states.py +37 -0
  35. roller/claude_capture.py +185 -0
  36. roller/claude_code_actf.py +594 -0
  37. roller/cli.py +1732 -0
  38. roller/codex_actf.py +584 -0
  39. roller/codex_capture.py +99 -0
  40. roller/codex_tools.py +137 -0
  41. roller/collector.py +818 -0
  42. roller/compaction_sft.py +672 -0
  43. roller/conversation.py +432 -0
  44. roller/deepseek_actf.py +135 -0
  45. roller/harness_registry.py +61 -0
  46. roller/harnesses/__init__.py +29 -0
  47. roller/harnesses/base.py +64 -0
  48. roller/harnesses/bridge.py +448 -0
  49. roller/harnesses/claude_code.py +839 -0
  50. roller/harnesses/codex.py +808 -0
  51. roller/harnesses/config_overrides.py +100 -0
  52. roller/harnesses/container.py +206 -0
  53. roller/harnesses/deepseek.py +104 -0
  54. roller/harnesses/docker.py +72 -0
  55. roller/harnesses/gateway_ports.py +155 -0
  56. roller/harnesses/hermes.py +119 -0
  57. roller/harnesses/openclaw.py +1605 -0
  58. roller/harnesses/opencode.py +193 -0
  59. roller/harnesses/pi.py +111 -0
  60. roller/hermes_actf.py +147 -0
  61. roller/image_build.py +206 -0
  62. roller/integrity.py +332 -0
  63. roller/model_protocols.py +201 -0
  64. roller/models.py +169 -0
  65. roller/openclaw_capture.py +140 -0
  66. roller/openclaw_delivery.py +211 -0
  67. roller/openclaw_usage.py +59 -0
  68. roller/opencode_actf.py +305 -0
  69. roller/opencode_capture.py +96 -0
  70. roller/pi_actf.py +172 -0
  71. roller/response_metadata.py +89 -0
  72. roller/run_lock.py +297 -0
  73. roller/sft.py +471 -0
  74. roller/storage.py +53 -0
  75. roller/tasks.py +689 -0
  76. roller/thinking.py +50 -0
  77. roller/thinking_capabilities.py +152 -0
  78. roller/trajectory_audit.py +386 -0
  79. roller/usage.py +168 -0
  80. roller/version.py +1 -0
  81. roller/viewer/__init__.py +3 -0
  82. roller/viewer/app.py +722 -0
  83. roller/viewer/static/app.js +752 -0
  84. roller/viewer/static/index.html +105 -0
  85. roller/viewer/static/style.css +213 -0
  86. roller/wire_trace.py +650 -0
  87. roller_agent-0.2.0.dist-info/METADATA +397 -0
  88. roller_agent-0.2.0.dist-info/RECORD +92 -0
  89. roller_agent-0.2.0.dist-info/WHEEL +5 -0
  90. roller_agent-0.2.0.dist-info/entry_points.txt +2 -0
  91. roller_agent-0.2.0.dist-info/licenses/LICENSE +201 -0
  92. 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