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
@@ -0,0 +1,49 @@
1
+ """QwenClawBench adapter package."""
2
+
3
+ from roller.benches.qwenclaw.adapter import (
4
+ DATASET_DIR_NAME,
5
+ TASK_SCHEMA,
6
+ QwenClawBenchAdapter,
7
+ )
8
+ from roller.benches.qwenclaw.grader_runtime import (
9
+ AUTO_PENALTY_THRESHOLD,
10
+ GRADE_TIMEOUT_SECONDS,
11
+ JUDGE_ENV_NAMES,
12
+ OUTPUT_DIR_NAME,
13
+ WORKSPACE_CONTAINER_PATH,
14
+ QwenClawGradedHarness,
15
+ build_grader_source,
16
+ )
17
+ from roller.benches.qwenclaw.report import aggregate_samples
18
+ from roller.benches.qwenclaw.task_parser import (
19
+ DEFAULT_TIMEOUT_SECONDS,
20
+ QwenClawTask,
21
+ parse_task_markdown,
22
+ split_sections,
23
+ )
24
+ from roller.benches.qwenclaw.workspace import (
25
+ materialize_workspace,
26
+ workspace_compatibility_script,
27
+ )
28
+ from roller.benches.transcripts import session_trajectory
29
+
30
+ __all__ = [
31
+ "AUTO_PENALTY_THRESHOLD",
32
+ "DATASET_DIR_NAME",
33
+ "DEFAULT_TIMEOUT_SECONDS",
34
+ "GRADE_TIMEOUT_SECONDS",
35
+ "JUDGE_ENV_NAMES",
36
+ "OUTPUT_DIR_NAME",
37
+ "TASK_SCHEMA",
38
+ "WORKSPACE_CONTAINER_PATH",
39
+ "QwenClawBenchAdapter",
40
+ "QwenClawGradedHarness",
41
+ "QwenClawTask",
42
+ "aggregate_samples",
43
+ "build_grader_source",
44
+ "materialize_workspace",
45
+ "parse_task_markdown",
46
+ "session_trajectory",
47
+ "split_sections",
48
+ "workspace_compatibility_script",
49
+ ]
@@ -0,0 +1,167 @@
1
+ """QwenClawBench adapter assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Iterable, Sequence
7
+ from dataclasses import replace
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from roller.benches.base import (
12
+ BenchAdapter,
13
+ BenchBinding,
14
+ BenchFormatError,
15
+ GradeSpec,
16
+ validate_timeout_multiplier,
17
+ )
18
+ from roller.benches.qwenclaw.grader_runtime import (
19
+ GRADE_TIMEOUT_SECONDS,
20
+ JUDGE_ENV_NAMES,
21
+ OUTPUT_DIR_NAME,
22
+ WORKSPACE_CONTAINER_PATH,
23
+ QwenClawGradedHarness,
24
+ build_grader_source,
25
+ )
26
+ from roller.benches.qwenclaw.report import aggregate_samples
27
+ from roller.benches.qwenclaw.task_parser import parse_task_markdown
28
+ from roller.benches.qwenclaw.workspace import materialize_workspace
29
+
30
+ TASK_SCHEMA = "roller.task/v1"
31
+ DATASET_DIR_NAME = "qwenclawbench-v1.1-100"
32
+
33
+
34
+ class QwenClawBenchAdapter(BenchAdapter):
35
+ name = "qwenclawbench"
36
+ description = "QwenClawBench — 100 real-user OpenClaw tasks across 8 domains"
37
+
38
+ def __init__(self, cache_root: Path | None = None) -> None:
39
+ self.cache_root = cache_root
40
+
41
+ def load(
42
+ self,
43
+ root: Path,
44
+ *,
45
+ categories: Sequence[str] | None = None,
46
+ task_ids: Sequence[str] | None = None,
47
+ variant: str | None = None,
48
+ timeout_multiplier: float = 1.0,
49
+ ) -> list[dict[str, Any]]:
50
+ if variant is not None:
51
+ raise BenchFormatError(f"{self.name}: this bench has no variants")
52
+ multiplier = validate_timeout_multiplier(timeout_multiplier)
53
+ dataset = self._dataset_root(root)
54
+ tasks_root = dataset / "tasks"
55
+ assets_root = dataset / "assets"
56
+ wanted_categories = set(categories or ())
57
+ wanted_ids = set(task_ids or ())
58
+ rows: list[dict[str, Any]] = []
59
+ seen_ids: set[str] = set()
60
+ available_categories: set[str] = set()
61
+
62
+ for task_path in sorted(tasks_root.glob("task_*.md")):
63
+ task = parse_task_markdown(
64
+ task_path.read_text(encoding="utf-8"),
65
+ str(task_path),
66
+ source_path=task_path,
67
+ )
68
+ if task.task_id in seen_ids:
69
+ raise BenchFormatError(
70
+ f"duplicate QwenClawBench task id: {task.task_id}"
71
+ )
72
+ seen_ids.add(task.task_id)
73
+ if task.category:
74
+ available_categories.add(task.category)
75
+ if wanted_categories and task.category not in wanted_categories:
76
+ continue
77
+ if wanted_ids and task.task_id not in wanted_ids:
78
+ continue
79
+
80
+ workspace = materialize_workspace(task, assets_root, self.cache_root)
81
+ env_names = (
82
+ JUDGE_ENV_NAMES if task.grading_type in {"llm_judge", "hybrid"} else ()
83
+ )
84
+ binding = BenchBinding(
85
+ name=self.name,
86
+ category=task.category or None,
87
+ metadata={
88
+ "upstream_task_id": task.task_id,
89
+ "name": task.name,
90
+ "subcategory": task.subcategory,
91
+ "grading_type": task.grading_type,
92
+ "grading_weights": task.grading_weights,
93
+ "input_modality": task.input_modality,
94
+ "external_dependency": task.external_dependency,
95
+ "run_index": 1,
96
+ "runs_per_task": 1,
97
+ },
98
+ grade=GradeSpec(
99
+ code=build_grader_source(task),
100
+ entrypoint="roller_grade",
101
+ env_names=env_names,
102
+ timeout_seconds=GRADE_TIMEOUT_SECONDS,
103
+ needs_transcript=True,
104
+ workspace_path=WORKSPACE_CONTAINER_PATH,
105
+ score_key="overall_score",
106
+ ),
107
+ )
108
+ rows.append(
109
+ {
110
+ "schema_version": TASK_SCHEMA,
111
+ "id": task.task_id,
112
+ "harness": "openclaw",
113
+ "prompt": task.prompt,
114
+ "workspace": str(workspace),
115
+ "files": {"output_dir": OUTPUT_DIR_NAME},
116
+ "timeout_seconds": max(
117
+ 1, math.ceil(task.timeout_seconds * multiplier)
118
+ ),
119
+ "bench": binding.as_dict(),
120
+ }
121
+ )
122
+
123
+ missing_categories = wanted_categories - available_categories
124
+ if missing_categories:
125
+ raise BenchFormatError(
126
+ "unknown QwenClawBench categories: "
127
+ + ", ".join(sorted(missing_categories))
128
+ )
129
+ missing_ids = wanted_ids - seen_ids
130
+ if missing_ids:
131
+ raise BenchFormatError(
132
+ "unknown QwenClawBench task ids: " + ", ".join(sorted(missing_ids))
133
+ )
134
+ return rows
135
+
136
+ def wrap_harness(self, harness, binding, *, grading_semaphore=None):
137
+ if binding.grade is None:
138
+ return harness
139
+ grade = replace(
140
+ binding.grade, transcript_path="/tmp/roller-qwenclaw-transcript.json"
141
+ )
142
+ return QwenClawGradedHarness(
143
+ harness,
144
+ replace(binding, grade=grade),
145
+ grading_semaphore=grading_semaphore,
146
+ )
147
+
148
+ def aggregate(self, samples: Iterable[dict[str, Any]]) -> dict[str, Any]:
149
+ return aggregate_samples(samples)
150
+
151
+ @staticmethod
152
+ def _dataset_root(root: Path) -> Path:
153
+ candidates = [
154
+ root,
155
+ root / "data" / DATASET_DIR_NAME,
156
+ ]
157
+ data_root = root / "data"
158
+ if data_root.is_dir():
159
+ candidates.extend(
160
+ path for path in sorted(data_root.iterdir()) if path.is_dir()
161
+ )
162
+ for candidate in candidates:
163
+ if (candidate / "tasks").is_dir() and (candidate / "assets").is_dir():
164
+ return candidate.resolve()
165
+ raise BenchFormatError(
166
+ f"{root}: expected tasks/ and assets/, or data/{DATASET_DIR_NAME}/"
167
+ )
@@ -0,0 +1,395 @@
1
+ """Self-contained QwenClawBench grader generation and harness integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ import re
8
+ import tempfile
9
+
10
+ from roller.benches.grading import (
11
+ GradedHarness,
12
+ GradeOutcome,
13
+ workspace_container_path,
14
+ )
15
+ from roller.benches.qwenclaw.task_parser import QwenClawTask
16
+ from roller.benches.qwenclaw.workspace import workspace_compatibility_script
17
+ from roller.benches.transcripts import grader_messages, session_trajectory
18
+
19
+ IMPORT_LINE_RE = re.compile(
20
+ r"^(?P<indent>[ \t]*)import\s+(?P<names>[A-Za-z0-9_., \t]+)$",
21
+ re.MULTILINE,
22
+ )
23
+
24
+ WORKSPACE_CONTAINER_PATH = "/home/node/workspace"
25
+ OUTPUT_DIR_NAME = ".roller_outputs"
26
+ GRADE_TIMEOUT_SECONDS = 1800
27
+ AUTO_PENALTY_THRESHOLD = 0.75
28
+
29
+ JUDGE_ENV_NAMES = (
30
+ "QWENCLAW_JUDGE_BASE_URL",
31
+ "QWENCLAW_JUDGE_API_KEY",
32
+ "QWENCLAW_JUDGE_MODEL",
33
+ "QWENCLAW_JUDGE_MAX_RETRIES",
34
+ "JUDGE_BASE_URL",
35
+ "JUDGE_API_KEY",
36
+ "JUDGE_MODEL",
37
+ )
38
+
39
+ _GRADER_RUNTIME = r"""
40
+ import json as _roller_json
41
+ import math as _roller_math
42
+ import os as _roller_os
43
+ import re as _roller_re
44
+ import shutil as _roller_shutil
45
+ import time as _roller_time
46
+ import urllib.error as _roller_urlerror
47
+ import urllib.request as _roller_urlrequest
48
+ from pathlib import Path as _RollerPath
49
+
50
+ _roller_automated_grade = globals().get("grade")
51
+ _ROLLER_GRADING_TYPE = __ROLLER_GRADING_TYPE__
52
+ _ROLLER_PROMPT = __ROLLER_PROMPT__
53
+ _ROLLER_EXPECTED = __ROLLER_EXPECTED__
54
+ _ROLLER_RUBRIC = __ROLLER_RUBRIC__
55
+ _ROLLER_WEIGHTS = __ROLLER_WEIGHTS__
56
+ _ROLLER_OUTPUT_DIR = __ROLLER_OUTPUT_DIR__
57
+
58
+ def _roller_numeric(scores):
59
+ result = {}
60
+ for key, value in (scores or {}).items():
61
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
62
+ result[str(key)] = float(value)
63
+ return result
64
+
65
+ def _roller_average(scores):
66
+ values = list(_roller_numeric(scores).values())
67
+ return sum(values) / len(values) if values else 0.0
68
+
69
+ def _roller_text(content):
70
+ if isinstance(content, str):
71
+ return content
72
+ if isinstance(content, dict):
73
+ for key in ("text", "thinking", "reasoning"):
74
+ value = content.get(key)
75
+ if isinstance(value, str):
76
+ return value
77
+ return ""
78
+ if isinstance(content, list):
79
+ return "\n".join(filter(None, (_roller_text(item) for item in content)))
80
+ return ""
81
+
82
+ def _roller_summary(transcript):
83
+ parts = []
84
+ for event in transcript or []:
85
+ if not isinstance(event, dict) or event.get("type") != "message":
86
+ continue
87
+ message = event.get("message") or {}
88
+ role = message.get("role")
89
+ content = message.get("content") or []
90
+ if role == "assistant":
91
+ for item in content:
92
+ item_type = item.get("type") if isinstance(item, dict) else None
93
+ if isinstance(item, str) or item_type == "text":
94
+ text = _roller_text(item)
95
+ if text:
96
+ parts.append("Assistant: %s" % text[:2000])
97
+ elif item_type in ("thinking", "reasoning"):
98
+ text = _roller_text(item)
99
+ if text:
100
+ parts.append("Reasoning: %s" % text[:2000])
101
+ elif isinstance(item, dict) and item.get("type") in ("toolCall", "tool_use"):
102
+ arguments = item.get("arguments", item.get("input", {}))
103
+ parts.append("Tool: %s(%s)" % (
104
+ item.get("name"),
105
+ _roller_json.dumps(arguments, ensure_ascii=False),
106
+ ))
107
+ elif role == "toolResult":
108
+ text = _roller_text(content)
109
+ if text:
110
+ parts.append("Result: %s" % text[:2000])
111
+ elif role == "user":
112
+ text = _roller_text(content)
113
+ if text:
114
+ parts.append("User: %s" % text[:2000])
115
+ return "\n".join(parts)
116
+
117
+ def _roller_judge_prompt(transcript):
118
+ return (
119
+ "You are a grading function. Your ONLY job is to output a single JSON object.\n\n"
120
+ "CRITICAL RULES:\n"
121
+ "- Do NOT use any tools (no Read, Write, exec, or any other tool calls)\n"
122
+ "- Do NOT create files or run commands\n"
123
+ "- Do NOT write any prose, explanation, or commentary outside the JSON\n"
124
+ "- Respond with ONLY a JSON object — nothing else\n\n"
125
+ "Be a strict evaluator. Reserve 1.0 for genuinely excellent performance. "
126
+ "An average acceptable completion should score around 0.6-0.7. "
127
+ "Deduct points for unnecessary steps, verbose output, and inefficient tool usage.\n\n"
128
+ "## Task\n%s\n\n## Expected Behavior\n%s\n\n"
129
+ "## Agent Transcript (summarized)\n%s\n\n## Grading Rubric\n%s\n\n"
130
+ "Score each criterion from 0.0 to 1.0.\n\n"
131
+ "Respond with ONLY this JSON structure (no markdown, no code fences, no extra text):\n"
132
+ '{"scores": {"criterion_name": 0.0}, "total": 0.0, "notes": "brief justification"}'
133
+ ) % (_ROLLER_PROMPT, _ROLLER_EXPECTED, _roller_summary(transcript), _ROLLER_RUBRIC)
134
+
135
+ def _roller_json_objects(text):
136
+ objects = []
137
+ depth = 0
138
+ current = []
139
+ for char in text:
140
+ if char == "{":
141
+ if depth == 0:
142
+ current = []
143
+ depth += 1
144
+ if depth:
145
+ current.append(char)
146
+ if char == "}" and depth:
147
+ depth -= 1
148
+ if depth == 0:
149
+ objects.append("".join(current))
150
+ return objects
151
+
152
+ def _roller_parse_judge(text):
153
+ text = (text or "").strip()
154
+ fenced = _roller_re.search(r"```(?:json)?\s*(.*?)\s*```", text, _roller_re.DOTALL)
155
+ candidates = ([fenced.group(1)] if fenced else []) + list(reversed(_roller_json_objects(text)))
156
+ parsed = {}
157
+ for candidate in candidates:
158
+ try:
159
+ value = _roller_json.loads(candidate)
160
+ except Exception:
161
+ continue
162
+ if isinstance(value, dict):
163
+ parsed = value
164
+ if "scores" in value:
165
+ break
166
+ scores = {}
167
+ source_scores = parsed.get("scores", parsed.get("criteria_scores", {}))
168
+ if isinstance(source_scores, dict):
169
+ for key, value in source_scores.items():
170
+ if isinstance(value, dict):
171
+ value = value.get("score")
172
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
173
+ if not _roller_math.isfinite(value):
174
+ raise ValueError("judge criterion score must be finite: %s" % key)
175
+ scores[str(key)] = float(value)
176
+ total = parsed.get("total", parsed.get("score"))
177
+ if not isinstance(total, (int, float)) or isinstance(total, bool):
178
+ if not scores:
179
+ raise ValueError(
180
+ "judge response did not contain a numeric total or criterion score"
181
+ )
182
+ total = _roller_average(scores)
183
+ if not _roller_math.isfinite(total):
184
+ raise ValueError("judge total score must be finite")
185
+ return scores, max(0.0, min(1.0, float(total))), str(
186
+ parsed.get("notes", parsed.get("justification", parsed.get("reasoning", "")))
187
+ )
188
+
189
+ def _roller_call_judge(transcript):
190
+ base = (
191
+ _roller_os.environ.get("QWENCLAW_JUDGE_BASE_URL")
192
+ or _roller_os.environ.get("JUDGE_BASE_URL")
193
+ or ""
194
+ ).strip()
195
+ key = (
196
+ _roller_os.environ.get("QWENCLAW_JUDGE_API_KEY")
197
+ or _roller_os.environ.get("JUDGE_API_KEY")
198
+ or ""
199
+ ).strip()
200
+ model = (
201
+ _roller_os.environ.get("QWENCLAW_JUDGE_MODEL")
202
+ or _roller_os.environ.get("JUDGE_MODEL")
203
+ or "claude-opus-4-5-20251101"
204
+ ).strip()
205
+ if not base or not key:
206
+ raise RuntimeError(
207
+ "LLM judge requires QWENCLAW_JUDGE_BASE_URL and "
208
+ "QWENCLAW_JUDGE_API_KEY (JUDGE_BASE_URL/JUDGE_API_KEY also accepted)"
209
+ )
210
+ endpoint = base.rstrip("/")
211
+ if not endpoint.endswith("/chat/completions"):
212
+ endpoint += "/chat/completions"
213
+ payload = _roller_json.dumps({
214
+ "model": model.split("/", 1)[-1] if "/" in model else model,
215
+ "messages": [{"role": "user", "content": _roller_judge_prompt(transcript)}],
216
+ "temperature": 0.0,
217
+ "max_tokens": 20480,
218
+ }).encode("utf-8")
219
+ retries = int(_roller_os.environ.get("QWENCLAW_JUDGE_MAX_RETRIES", "3"))
220
+ retries = max(1, min(100, retries))
221
+ last_error = None
222
+ for attempt in range(retries):
223
+ request = _roller_urlrequest.Request(
224
+ endpoint,
225
+ data=payload,
226
+ headers={"Content-Type": "application/json", "Authorization": "Bearer " + key},
227
+ method="POST",
228
+ )
229
+ try:
230
+ with _roller_urlrequest.urlopen(request, timeout=600) as response:
231
+ body = _roller_json.loads(response.read().decode("utf-8"))
232
+ choices = body.get("choices") or []
233
+ if not choices:
234
+ raise RuntimeError("judge API returned no choices")
235
+ return _roller_parse_judge((choices[0].get("message") or {}).get("content", ""))
236
+ except Exception as exc:
237
+ last_error = exc
238
+ if attempt + 1 < retries:
239
+ _roller_time.sleep(min(5 * (2 ** attempt), 30))
240
+ raise RuntimeError("judge API failed after %d attempt(s): %s" % (retries, last_error))
241
+
242
+ def _roller_snapshot(workspace_path):
243
+ source = _RollerPath(workspace_path)
244
+ destination = source / _ROLLER_OUTPUT_DIR
245
+ if destination.exists():
246
+ _roller_shutil.rmtree(destination)
247
+ destination.mkdir(parents=True)
248
+ for item in source.iterdir():
249
+ if item.name in {_ROLLER_OUTPUT_DIR, ".git"}:
250
+ continue
251
+ target = destination / item.name
252
+ if item.is_symlink():
253
+ continue
254
+ if item.is_dir():
255
+ _roller_shutil.copytree(item, target, symlinks=True)
256
+ elif item.is_file():
257
+ _roller_shutil.copy2(item, target)
258
+
259
+ def roller_grade(transcript, workspace_path):
260
+ try:
261
+ automated_scores = {}
262
+ automated_score = 0.0
263
+ if _ROLLER_GRADING_TYPE in {"automated", "hybrid"}:
264
+ if not callable(_roller_automated_grade):
265
+ raise RuntimeError("automated grader function is missing")
266
+ automated_scores = _roller_numeric(
267
+ _roller_automated_grade(transcript, workspace_path)
268
+ )
269
+ automated_score = _roller_average(automated_scores)
270
+ judge_scores = {}
271
+ judge_score = 0.0
272
+ notes = ""
273
+ if _ROLLER_GRADING_TYPE in {"llm_judge", "hybrid"}:
274
+ judge_scores, judge_score, notes = _roller_call_judge(transcript)
275
+ if _ROLLER_GRADING_TYPE == "automated":
276
+ overall = simple = automated_score
277
+ elif _ROLLER_GRADING_TYPE == "llm_judge":
278
+ overall = simple = judge_score
279
+ else:
280
+ auto_weight = float(_ROLLER_WEIGHTS.get("automated", 0.5))
281
+ judge_weight = float(_ROLLER_WEIGHTS.get("llm_judge", 0.5))
282
+ weight = auto_weight + judge_weight
283
+ if weight <= 0:
284
+ auto_weight = judge_weight = 0.5
285
+ weight = 1.0
286
+ simple = (automated_score * auto_weight + judge_score * judge_weight) / weight
287
+ adjusted = 0.0 if automated_score < 0.75 else judge_score
288
+ overall = (automated_score * auto_weight + adjusted * judge_weight) / weight
289
+ return {
290
+ "overall_score": overall,
291
+ "score_simple": simple,
292
+ "automated_score": automated_score,
293
+ "llm_judge_score": judge_score,
294
+ "automated": automated_scores,
295
+ "llm_judge": judge_scores,
296
+ "notes": notes,
297
+ }
298
+ finally:
299
+ _roller_snapshot(workspace_path)
300
+ """
301
+
302
+
303
+ def _remove_unused_yaml_import(code: str) -> str:
304
+ tree = ast.parse(code)
305
+ if any(
306
+ isinstance(node, ast.Name)
307
+ and node.id == "yaml"
308
+ and isinstance(node.ctx, ast.Load)
309
+ for node in ast.walk(tree)
310
+ ):
311
+ return code
312
+
313
+ def remove_yaml(match: re.Match[str]) -> str:
314
+ names = [name.strip() for name in match.group("names").split(",")]
315
+ if "yaml" not in names:
316
+ return match.group(0)
317
+ names.remove("yaml")
318
+ if not names:
319
+ return ""
320
+ return f"{match.group('indent')}import {', '.join(names)}"
321
+
322
+ return IMPORT_LINE_RE.sub(remove_yaml, code)
323
+
324
+
325
+ def build_grader_source(task: QwenClawTask) -> str:
326
+ automated = _remove_unused_yaml_import(task.automated_code)
327
+ runtime = (
328
+ _GRADER_RUNTIME.replace("__ROLLER_GRADING_TYPE__", repr(task.grading_type))
329
+ .replace("__ROLLER_PROMPT__", repr(task.prompt))
330
+ .replace("__ROLLER_EXPECTED__", repr(task.expected_behavior))
331
+ .replace("__ROLLER_RUBRIC__", repr(task.llm_judge_rubric))
332
+ .replace("__ROLLER_WEIGHTS__", repr(task.grading_weights))
333
+ .replace("__ROLLER_OUTPUT_DIR__", repr(OUTPUT_DIR_NAME))
334
+ )
335
+ return (automated + "\n\n" if automated else "") + runtime.strip() + "\n"
336
+
337
+
338
+ def _session_trajectory(session):
339
+ # Preserve the old module-level monkeypatch point after moving this class
340
+ # into a submodule.
341
+ from roller.benches import qwenclaw
342
+
343
+ return qwenclaw.session_trajectory(session)
344
+
345
+
346
+ class QwenClawGradedHarness(GradedHarness):
347
+ """Normalize each native journal before the upstream grader sees it."""
348
+
349
+ def _prepare_session(self, session) -> None:
350
+ super()._prepare_session(session)
351
+ workspace = workspace_container_path(session)
352
+ result = self.inner.runtime.run(
353
+ [
354
+ "exec",
355
+ session.container,
356
+ "/bin/sh",
357
+ "-c",
358
+ workspace_compatibility_script(workspace),
359
+ ],
360
+ timeout=30,
361
+ )
362
+ if result.returncode != 0:
363
+ detail = (result.stderr or result.stdout or "").strip()[-500:]
364
+ raise RuntimeError(
365
+ "QwenClawBench workspace compatibility setup failed"
366
+ + (f": {detail}" if detail else "")
367
+ )
368
+
369
+ def _grade(self, session, request, status):
370
+ if not getattr(session, "container_started", False):
371
+ return super()._grade(session, request, status)
372
+ try:
373
+ events = grader_messages(_session_trajectory(session))
374
+ with tempfile.NamedTemporaryFile(
375
+ "w", suffix=".json", encoding="utf-8"
376
+ ) as handle:
377
+ json.dump(events, handle, ensure_ascii=False)
378
+ handle.flush()
379
+ copied = self.inner.runtime.run(
380
+ [
381
+ "cp",
382
+ handle.name,
383
+ f"{session.container}:/tmp/roller-qwenclaw-transcript.json",
384
+ ],
385
+ timeout=60,
386
+ )
387
+ if copied.returncode:
388
+ raise RuntimeError(
389
+ "QwenClawBench normalized transcript staging failed"
390
+ )
391
+ return super()._grade(session, request, status)
392
+ except Exception as exc: # noqa: BLE001 - grading failures become score metadata
393
+ return GradeOutcome(
394
+ {"overall_score": 0.0, "error": str(exc)}, 0.0, str(exc)
395
+ )
@@ -0,0 +1,49 @@
1
+ """QwenClawBench sample aggregation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ from roller.benches.report import mean_aggregate, sample_score
9
+
10
+
11
+ def aggregate_samples(samples: Iterable[dict[str, Any]]) -> dict[str, Any]:
12
+ rows = list(samples)
13
+ report = mean_aggregate(rows)
14
+ grouped: dict[str, list[float]] = {}
15
+ for row in rows:
16
+ metadata = row.get("bench_metadata")
17
+ upstream_id = (
18
+ metadata.get("upstream_task_id")
19
+ if isinstance(metadata, dict)
20
+ else row.get("task_id")
21
+ )
22
+ scores = row.get("bench_scores")
23
+ value = (
24
+ sample_score(scores, row.get("bench_score_key") or "overall_score")
25
+ if isinstance(scores, dict)
26
+ else None
27
+ )
28
+ if isinstance(upstream_id, str):
29
+ grouped.setdefault(upstream_id, []).append(
30
+ value if value is not None else 0.0
31
+ )
32
+ task_means = {
33
+ task_id: sum(values) / len(values) for task_id, values in grouped.items()
34
+ }
35
+ perfect = sum(value >= 1.0 - 1e-9 for value in task_means.values())
36
+ report["average"] = (
37
+ round(sum(task_means.values()) / len(task_means), 6)
38
+ if task_means
39
+ else None
40
+ )
41
+ report["upstream_task_count"] = len(task_means)
42
+ report["strict_accuracy"] = (
43
+ round(perfect / len(task_means), 6) if task_means else None
44
+ )
45
+ report["perfect_task_count"] = perfect
46
+ report["runs_per_task"] = {
47
+ task_id: len(values) for task_id, values in sorted(grouped.items())
48
+ }
49
+ return report