benchkit-cli 0.1.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.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
benchkit/results.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
"""Result shaping: from finished tasks to the rows reports serialize.
|
|
2
|
+
|
|
3
|
+
The per-task record, the running sums a job collects as its tasks finish, the
|
|
4
|
+
job result built from them, and the paired direct-versus-Pi annotation applied
|
|
5
|
+
once a run is over. Nothing here talks to a model, so it works on plain records.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from statistics import median
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from benchkit import decision
|
|
15
|
+
from benchkit.benchmarks.base import Task
|
|
16
|
+
from benchkit.jobs import JobSpec, benchmark, option_count
|
|
17
|
+
from benchkit.metrics import throughput_metrics
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TaskRecord:
|
|
22
|
+
"""Outcome of a single task."""
|
|
23
|
+
|
|
24
|
+
index: int
|
|
25
|
+
task_id: str
|
|
26
|
+
passed: bool
|
|
27
|
+
tok_s: float
|
|
28
|
+
response_time_s: float
|
|
29
|
+
prompt: str
|
|
30
|
+
response: str
|
|
31
|
+
outcome: Literal[
|
|
32
|
+
"pass",
|
|
33
|
+
"fail",
|
|
34
|
+
"loop_killed",
|
|
35
|
+
"timeout",
|
|
36
|
+
"length_exceeded",
|
|
37
|
+
"harness_error",
|
|
38
|
+
"contaminated",
|
|
39
|
+
] = "fail"
|
|
40
|
+
perturbation: dict = field(default_factory=dict)
|
|
41
|
+
score: float = 0.0
|
|
42
|
+
error: str = ""
|
|
43
|
+
entry_point: str = ""
|
|
44
|
+
thinking: str = ""
|
|
45
|
+
output_tokens: int = 0
|
|
46
|
+
tokens_recovered: bool = False
|
|
47
|
+
done_reason: str = ""
|
|
48
|
+
timed_out: bool = False
|
|
49
|
+
loop_killed: bool = False
|
|
50
|
+
length_exceeded: bool = False
|
|
51
|
+
harness_error: bool = False
|
|
52
|
+
contaminated: bool = False
|
|
53
|
+
loop_kill_score: float = 0.0
|
|
54
|
+
loop_killed_at_s: float | None = None
|
|
55
|
+
trace_status: str = "unavailable"
|
|
56
|
+
thinking_time_s: float = 0.0
|
|
57
|
+
time_to_first_answer_s: float | None = None
|
|
58
|
+
loop_state: str = "unavailable"
|
|
59
|
+
loop_score: float = 0.0
|
|
60
|
+
loop_source: str = "none"
|
|
61
|
+
loop_detected_at_s: float | None = None
|
|
62
|
+
repeated_ngram_coverage: float = 0.0
|
|
63
|
+
max_window_similarity: float = 0.0
|
|
64
|
+
low_novelty_windows: int = 0
|
|
65
|
+
max_repeated_block: int = 0
|
|
66
|
+
loop_evidence: str = "none"
|
|
67
|
+
active_cycle: bool = False
|
|
68
|
+
recovered_cycle: bool = False
|
|
69
|
+
cycle_period_tokens: int = 0
|
|
70
|
+
cycle_repetitions: int = 0
|
|
71
|
+
repeated_suffix_tokens: int = 0
|
|
72
|
+
harness: str = "direct"
|
|
73
|
+
harness_version: str = ""
|
|
74
|
+
input_tokens: int = 0
|
|
75
|
+
model_turns: int = 1
|
|
76
|
+
tool_calls: int = 0
|
|
77
|
+
tool_trace: list[dict] = field(default_factory=list)
|
|
78
|
+
pi_scaffold: dict = field(default_factory=dict)
|
|
79
|
+
attempts: list[dict] = field(default_factory=list)
|
|
80
|
+
repair_attempts_used: int = 0
|
|
81
|
+
repair_feedback: list[str] = field(default_factory=list)
|
|
82
|
+
first_attempt_score: float = 0.0
|
|
83
|
+
repaired: bool = False
|
|
84
|
+
workspace: dict = field(default_factory=dict)
|
|
85
|
+
decision: dict = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def label(self) -> str:
|
|
89
|
+
return (
|
|
90
|
+
f"{self.task_id} ({self.entry_point})" if self.entry_point else self.task_id
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _repair_score_curve(records: list[TaskRecord], repair_attempts: int) -> list[dict]:
|
|
95
|
+
"""Return aggregate score and incremental gains after each repair round."""
|
|
96
|
+
if not records or repair_attempts <= 0:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
def score_at(record: TaskRecord, round_number: int) -> float:
|
|
100
|
+
if record.attempts:
|
|
101
|
+
attempt = record.attempts[min(round_number, len(record.attempts) - 1)]
|
|
102
|
+
return float(attempt.get("score", 0.0))
|
|
103
|
+
if round_number == 0:
|
|
104
|
+
return record.first_attempt_score * 100
|
|
105
|
+
return record.score * 100
|
|
106
|
+
|
|
107
|
+
curve: list[dict] = []
|
|
108
|
+
previous_score = 0.0
|
|
109
|
+
for round_number in range(repair_attempts + 1):
|
|
110
|
+
scores = [score_at(record, round_number) for record in records]
|
|
111
|
+
aggregate = sum(scores) / len(scores)
|
|
112
|
+
previous_scores = (
|
|
113
|
+
[score_at(record, round_number - 1) for record in records]
|
|
114
|
+
if round_number
|
|
115
|
+
else scores
|
|
116
|
+
)
|
|
117
|
+
curve.append(
|
|
118
|
+
{
|
|
119
|
+
"round": round_number,
|
|
120
|
+
"score": round(aggregate, 1),
|
|
121
|
+
"delta_pp": round(aggregate - previous_score, 1)
|
|
122
|
+
if round_number
|
|
123
|
+
else 0.0,
|
|
124
|
+
"cumulative_delta_pp": round(aggregate - curve[0]["score"], 1)
|
|
125
|
+
if curve
|
|
126
|
+
else 0.0,
|
|
127
|
+
"attempted": sum(
|
|
128
|
+
len(record.attempts) > round_number for record in records
|
|
129
|
+
)
|
|
130
|
+
if round_number
|
|
131
|
+
else len(records),
|
|
132
|
+
"new_passes": sum(
|
|
133
|
+
before < 100 <= after
|
|
134
|
+
for before, after in zip(previous_scores, scores, strict=True)
|
|
135
|
+
)
|
|
136
|
+
if round_number
|
|
137
|
+
else sum(score >= 100 for score in scores),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
previous_score = aggregate
|
|
141
|
+
return curve
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class TaskOutcome:
|
|
146
|
+
"""One finalized task plus the raw metrics used by its job aggregate."""
|
|
147
|
+
|
|
148
|
+
record: TaskRecord
|
|
149
|
+
errors: int
|
|
150
|
+
eval_count: int
|
|
151
|
+
eval_duration_ns: int
|
|
152
|
+
response_time_s: float
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class JobTally:
|
|
157
|
+
"""Running sums for one job, folded in as its tasks finish."""
|
|
158
|
+
|
|
159
|
+
records_by_position: dict[int, TaskRecord] = field(default_factory=dict)
|
|
160
|
+
passed: int = 0
|
|
161
|
+
score_points: float = 0.0
|
|
162
|
+
scored_total: int = 0
|
|
163
|
+
errors: int = 0
|
|
164
|
+
total_tokens: int = 0
|
|
165
|
+
total_response_time: float = 0.0
|
|
166
|
+
throughput_tokens: int = 0
|
|
167
|
+
throughput_eval_ns: int = 0
|
|
168
|
+
throughput_response_time: float = 0.0
|
|
169
|
+
throughput_items: int = 0
|
|
170
|
+
|
|
171
|
+
def add(self, outcome: TaskOutcome) -> None:
|
|
172
|
+
record = outcome.record
|
|
173
|
+
self.records_by_position[record.index] = record
|
|
174
|
+
self.passed += int(record.passed)
|
|
175
|
+
self.score_points += record.score
|
|
176
|
+
self.scored_total += int(not record.harness_error and not record.contaminated)
|
|
177
|
+
self.errors += outcome.errors
|
|
178
|
+
self.total_tokens += outcome.eval_count
|
|
179
|
+
self.total_response_time += outcome.response_time_s
|
|
180
|
+
complete_terminal_metrics = not (
|
|
181
|
+
record.length_exceeded or record.timed_out
|
|
182
|
+
) or (
|
|
183
|
+
outcome.eval_count > 0
|
|
184
|
+
and outcome.eval_duration_ns > 0
|
|
185
|
+
and outcome.response_time_s > 0
|
|
186
|
+
and not record.tokens_recovered
|
|
187
|
+
)
|
|
188
|
+
if (
|
|
189
|
+
not (record.loop_killed or record.harness_error)
|
|
190
|
+
and complete_terminal_metrics
|
|
191
|
+
):
|
|
192
|
+
self.throughput_tokens += outcome.eval_count
|
|
193
|
+
self.throughput_eval_ns += outcome.eval_duration_ns
|
|
194
|
+
self.throughput_response_time += outcome.response_time_s
|
|
195
|
+
self.throughput_items += 1
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def job_result(
|
|
199
|
+
job: JobSpec,
|
|
200
|
+
bench: object,
|
|
201
|
+
tasks: list[Task],
|
|
202
|
+
tally: JobTally,
|
|
203
|
+
*,
|
|
204
|
+
slice_spec: str | None,
|
|
205
|
+
concurrency: int,
|
|
206
|
+
wall_time_s: float,
|
|
207
|
+
loop_kill_enabled: bool,
|
|
208
|
+
loop_kill_percent: float,
|
|
209
|
+
loop_kill_seconds: float,
|
|
210
|
+
) -> dict:
|
|
211
|
+
"""Build the result row for a job that finished at least one task."""
|
|
212
|
+
records = [
|
|
213
|
+
tally.records_by_position[position]
|
|
214
|
+
for position in sorted(tally.records_by_position)
|
|
215
|
+
]
|
|
216
|
+
total_time = round(wall_time_s, 1)
|
|
217
|
+
completed = len(records)
|
|
218
|
+
scored_records = [
|
|
219
|
+
record
|
|
220
|
+
for record in records
|
|
221
|
+
if not record.harness_error and not record.contaminated
|
|
222
|
+
]
|
|
223
|
+
scored_total = len(scored_records)
|
|
224
|
+
throughput_coverage = (
|
|
225
|
+
tally.throughput_response_time / tally.total_response_time
|
|
226
|
+
if tally.total_response_time > 0
|
|
227
|
+
else tally.throughput_items / completed
|
|
228
|
+
if completed
|
|
229
|
+
else 0.0
|
|
230
|
+
)
|
|
231
|
+
throughput_wall_time = wall_time_s * throughput_coverage
|
|
232
|
+
throughput = throughput_metrics(
|
|
233
|
+
output_tokens=tally.throughput_tokens,
|
|
234
|
+
generation_time_s=tally.throughput_eval_ns / 1e9,
|
|
235
|
+
request_time_s=tally.throughput_response_time,
|
|
236
|
+
wall_time_s=throughput_wall_time,
|
|
237
|
+
)
|
|
238
|
+
tok_s_per_stream = round(throughput["tok_s_per_stream"], 1)
|
|
239
|
+
tok_s_aggregate = round(throughput["tok_s_aggregate"], 1)
|
|
240
|
+
concurrency_eff = round(
|
|
241
|
+
min(float(concurrency), throughput["concurrency_eff"]),
|
|
242
|
+
2,
|
|
243
|
+
)
|
|
244
|
+
score = (
|
|
245
|
+
sum(record.score for record in scored_records) / scored_total * 100
|
|
246
|
+
if scored_total
|
|
247
|
+
else 0.0
|
|
248
|
+
)
|
|
249
|
+
loops = sum(record.loop_state == "looping" for record in records)
|
|
250
|
+
suspected = sum(
|
|
251
|
+
record.loop_state == "suspected" and not record.recovered_cycle
|
|
252
|
+
for record in records
|
|
253
|
+
)
|
|
254
|
+
recovered = sum(record.recovered_cycle for record in records)
|
|
255
|
+
timeouts = sum(record.timed_out for record in records)
|
|
256
|
+
loop_kills = sum(record.loop_killed for record in records)
|
|
257
|
+
length_exceeded = sum(record.length_exceeded for record in records)
|
|
258
|
+
harness_errors = sum(record.harness_error for record in records)
|
|
259
|
+
contaminated_records = [record for record in records if record.contaminated]
|
|
260
|
+
failures = sum(record.outcome == "fail" for record in records)
|
|
261
|
+
traced = sum(record.trace_status != "unavailable" for record in records)
|
|
262
|
+
latency_records = [
|
|
263
|
+
record
|
|
264
|
+
for record in records
|
|
265
|
+
if not (
|
|
266
|
+
record.loop_killed
|
|
267
|
+
or record.timed_out
|
|
268
|
+
or record.length_exceeded
|
|
269
|
+
or record.harness_error
|
|
270
|
+
)
|
|
271
|
+
and record.time_to_first_answer_s is not None
|
|
272
|
+
]
|
|
273
|
+
thinking_times = [record.thinking_time_s for record in latency_records]
|
|
274
|
+
answer_times = [float(record.time_to_first_answer_s) for record in latency_records]
|
|
275
|
+
pi_scaffold = next(
|
|
276
|
+
(record.pi_scaffold for record in records if record.pi_scaffold), {}
|
|
277
|
+
)
|
|
278
|
+
pi_metadata = {f"pi_{key}": value for key, value in pi_scaffold.items()}
|
|
279
|
+
agentic = [
|
|
280
|
+
record.workspace.get("agentic_metrics", {})
|
|
281
|
+
for record in records
|
|
282
|
+
if isinstance(record.workspace.get("agentic_metrics"), dict)
|
|
283
|
+
]
|
|
284
|
+
schema_valid = sum(int(item.get("tool_schema_valid_calls", 0)) for item in agentic)
|
|
285
|
+
schema_invalid = sum(
|
|
286
|
+
int(item.get("tool_schema_invalid_calls", 0)) for item in agentic
|
|
287
|
+
)
|
|
288
|
+
errored_calls = sum(int(item.get("errored_tool_calls", 0)) for item in agentic)
|
|
289
|
+
recoveries = sum(int(item.get("post_error_recoveries", 0)) for item in agentic)
|
|
290
|
+
redundant_calls = sum(int(item.get("redundant_tool_calls", 0)) for item in agentic)
|
|
291
|
+
destructive_actions = sum(
|
|
292
|
+
int(item.get("destructive_action_count", 0)) for item in agentic
|
|
293
|
+
)
|
|
294
|
+
observed_calls = schema_valid + schema_invalid
|
|
295
|
+
solved_records = [record for record in records if record.passed]
|
|
296
|
+
repair_score_curve = _repair_score_curve(scored_records, job.repair_attempts)
|
|
297
|
+
repair_round_columns = {
|
|
298
|
+
key: value
|
|
299
|
+
for point in repair_score_curve[1:]
|
|
300
|
+
for key, value in {
|
|
301
|
+
f"repair_round_{point['round']}_score": point["score"],
|
|
302
|
+
f"repair_round_{point['round']}_delta_pp": point["delta_pp"],
|
|
303
|
+
f"repair_round_{point['round']}_cumulative_delta_pp": point[
|
|
304
|
+
"cumulative_delta_pp"
|
|
305
|
+
],
|
|
306
|
+
f"repair_round_{point['round']}_attempted": point["attempted"],
|
|
307
|
+
f"repair_round_{point['round']}_new_passes": point["new_passes"],
|
|
308
|
+
}.items()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
result = {
|
|
312
|
+
"model": job.model,
|
|
313
|
+
"benchmark": bench.name,
|
|
314
|
+
"benchmark_label": job.benchmark_label,
|
|
315
|
+
"variant": job.variant,
|
|
316
|
+
"harness": job.harness,
|
|
317
|
+
"harness_label": job.harness_label,
|
|
318
|
+
"harness_version": (
|
|
319
|
+
next(
|
|
320
|
+
(
|
|
321
|
+
record.harness_version
|
|
322
|
+
for record in records
|
|
323
|
+
if record.harness_version
|
|
324
|
+
),
|
|
325
|
+
"",
|
|
326
|
+
)
|
|
327
|
+
),
|
|
328
|
+
"score": round(score, 1),
|
|
329
|
+
"passed": tally.passed,
|
|
330
|
+
"total": completed,
|
|
331
|
+
"scored_total": scored_total,
|
|
332
|
+
# Keep ``tok_s`` as a compatibility alias for consumers of older
|
|
333
|
+
# BenchKit JSON. New displays use the explicitly named metrics.
|
|
334
|
+
"tok_s": tok_s_per_stream,
|
|
335
|
+
"tok_s_per_stream": tok_s_per_stream,
|
|
336
|
+
"tok_s_aggregate": tok_s_aggregate,
|
|
337
|
+
"concurrency_eff": concurrency_eff,
|
|
338
|
+
"throughput_coverage": round(throughput_coverage * 100, 1),
|
|
339
|
+
"throughput_items": tally.throughput_items,
|
|
340
|
+
"throughput_wall_time": round(throughput_wall_time, 3),
|
|
341
|
+
"total_output_tokens": tally.total_tokens,
|
|
342
|
+
"throughput_output_tokens": tally.throughput_tokens,
|
|
343
|
+
"total_input_tokens": sum(record.input_tokens for record in records),
|
|
344
|
+
"model_turns": sum(record.model_turns for record in records),
|
|
345
|
+
"tool_calls": sum(record.tool_calls for record in records),
|
|
346
|
+
"tool_schema_valid_calls": schema_valid,
|
|
347
|
+
"tool_schema_invalid_calls": schema_invalid,
|
|
348
|
+
"tool_schema_validity_rate": round(schema_valid / observed_calls * 100, 1)
|
|
349
|
+
if observed_calls
|
|
350
|
+
else None,
|
|
351
|
+
"errored_tool_calls": errored_calls,
|
|
352
|
+
"post_error_recoveries": recoveries,
|
|
353
|
+
"post_error_recovery_rate": round(recoveries / errored_calls * 100, 1)
|
|
354
|
+
if errored_calls
|
|
355
|
+
else None,
|
|
356
|
+
"redundant_tool_calls": redundant_calls,
|
|
357
|
+
"redundant_action_rate": round(redundant_calls / observed_calls * 100, 1)
|
|
358
|
+
if observed_calls
|
|
359
|
+
else None,
|
|
360
|
+
"destructive_action_count": destructive_actions,
|
|
361
|
+
"destructive_action_rate": round(destructive_actions / observed_calls * 100, 1)
|
|
362
|
+
if observed_calls
|
|
363
|
+
else None,
|
|
364
|
+
"avg_turns_to_solve": round(
|
|
365
|
+
sum(record.model_turns for record in solved_records) / len(solved_records),
|
|
366
|
+
1,
|
|
367
|
+
)
|
|
368
|
+
if solved_records
|
|
369
|
+
else None,
|
|
370
|
+
"avg_tokens_to_solve": round(
|
|
371
|
+
sum(record.input_tokens + record.output_tokens for record in solved_records)
|
|
372
|
+
/ len(solved_records),
|
|
373
|
+
1,
|
|
374
|
+
)
|
|
375
|
+
if solved_records
|
|
376
|
+
else None,
|
|
377
|
+
"first_attempt_score": round(
|
|
378
|
+
sum(record.first_attempt_score for record in scored_records)
|
|
379
|
+
/ scored_total
|
|
380
|
+
* 100,
|
|
381
|
+
1,
|
|
382
|
+
)
|
|
383
|
+
if scored_total
|
|
384
|
+
else 0.0,
|
|
385
|
+
"repair_delta_pp": round(
|
|
386
|
+
score
|
|
387
|
+
- sum(record.first_attempt_score for record in scored_records)
|
|
388
|
+
/ scored_total
|
|
389
|
+
* 100,
|
|
390
|
+
1,
|
|
391
|
+
)
|
|
392
|
+
if scored_total
|
|
393
|
+
else 0.0,
|
|
394
|
+
"repair_attempted": sum(record.repair_attempts_used > 0 for record in records),
|
|
395
|
+
"repair_turns": sum(record.repair_attempts_used for record in records),
|
|
396
|
+
"repair_successes": sum(record.repaired for record in records),
|
|
397
|
+
"repair_score_curve": repair_score_curve,
|
|
398
|
+
**repair_round_columns,
|
|
399
|
+
"sum_generation_time": round(tally.throughput_eval_ns / 1e9, 3),
|
|
400
|
+
"sum_request_time": round(tally.throughput_response_time, 3),
|
|
401
|
+
"avg_response_time": round(
|
|
402
|
+
tally.throughput_response_time / tally.throughput_items, 1
|
|
403
|
+
)
|
|
404
|
+
if tally.throughput_items
|
|
405
|
+
else 0.0,
|
|
406
|
+
"total_time": total_time,
|
|
407
|
+
"slice": slice_spec,
|
|
408
|
+
"concurrency": concurrency,
|
|
409
|
+
"errors": tally.errors,
|
|
410
|
+
"harness_errors": harness_errors,
|
|
411
|
+
"contaminated": len(contaminated_records),
|
|
412
|
+
"contamination_by_language": {
|
|
413
|
+
language: sum(
|
|
414
|
+
record.workspace.get("language") == language
|
|
415
|
+
for record in contaminated_records
|
|
416
|
+
)
|
|
417
|
+
for language in sorted(
|
|
418
|
+
{
|
|
419
|
+
str(record.workspace.get("language"))
|
|
420
|
+
for record in contaminated_records
|
|
421
|
+
if record.workspace.get("language")
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
},
|
|
425
|
+
"contaminated_tasks": [
|
|
426
|
+
{
|
|
427
|
+
"task_id": record.task_id,
|
|
428
|
+
"language": record.workspace.get("language", ""),
|
|
429
|
+
"guard_hits": record.workspace.get("answer_key_guard_hits", []),
|
|
430
|
+
}
|
|
431
|
+
for record in contaminated_records
|
|
432
|
+
],
|
|
433
|
+
"length_exceeded": length_exceeded,
|
|
434
|
+
"failures": failures,
|
|
435
|
+
"timeouts": timeouts,
|
|
436
|
+
"loop_kills": loop_kills,
|
|
437
|
+
"loops": loops,
|
|
438
|
+
"suspected_loops": suspected,
|
|
439
|
+
"recovered_loops": recovered,
|
|
440
|
+
"loop_rate": round(loops / completed * 100, 1),
|
|
441
|
+
"trace_coverage": round(traced / completed * 100, 1),
|
|
442
|
+
"median_thinking_time": round(median(thinking_times), 1)
|
|
443
|
+
if thinking_times
|
|
444
|
+
else 0.0,
|
|
445
|
+
"median_time_to_answer": round(median(answer_times), 1)
|
|
446
|
+
if answer_times
|
|
447
|
+
else 0.0,
|
|
448
|
+
"loop_kill_enabled": loop_kill_enabled,
|
|
449
|
+
"loop_kill_percent": loop_kill_percent,
|
|
450
|
+
"loop_kill_seconds": loop_kill_seconds,
|
|
451
|
+
"tasks": [
|
|
452
|
+
{
|
|
453
|
+
"task_id": record.task_id,
|
|
454
|
+
"passed": record.passed,
|
|
455
|
+
"outcome": record.outcome,
|
|
456
|
+
"score": round(record.score * 100, 1),
|
|
457
|
+
"tok_s": record.tok_s,
|
|
458
|
+
"response_time_s": record.response_time_s,
|
|
459
|
+
"prompt": record.prompt,
|
|
460
|
+
"response": record.response,
|
|
461
|
+
"perturbation": record.perturbation or None,
|
|
462
|
+
"error": record.error,
|
|
463
|
+
"entry_point": record.entry_point,
|
|
464
|
+
"thinking": record.thinking,
|
|
465
|
+
"output_tokens": record.output_tokens,
|
|
466
|
+
"tokens_recovered": record.tokens_recovered,
|
|
467
|
+
"done_reason": record.done_reason,
|
|
468
|
+
"timed_out": record.timed_out,
|
|
469
|
+
"loop_killed": record.loop_killed,
|
|
470
|
+
"length_exceeded": record.length_exceeded,
|
|
471
|
+
"harness_error": record.harness_error,
|
|
472
|
+
"contaminated": record.contaminated,
|
|
473
|
+
"contamination_verdict": record.workspace.get(
|
|
474
|
+
"contamination_verdict", "CLEAR"
|
|
475
|
+
),
|
|
476
|
+
"loop_kill_score": record.loop_kill_score,
|
|
477
|
+
"loop_killed_at_s": record.loop_killed_at_s,
|
|
478
|
+
"trace_status": record.trace_status,
|
|
479
|
+
"thinking_time_s": record.thinking_time_s,
|
|
480
|
+
"time_to_first_answer_s": record.time_to_first_answer_s,
|
|
481
|
+
"loop_state": record.loop_state,
|
|
482
|
+
"loop_score": record.loop_score,
|
|
483
|
+
"loop_source": record.loop_source,
|
|
484
|
+
"loop_detected_at_s": record.loop_detected_at_s,
|
|
485
|
+
"repeated_ngram_coverage": (record.repeated_ngram_coverage),
|
|
486
|
+
"max_window_similarity": record.max_window_similarity,
|
|
487
|
+
"low_novelty_windows": record.low_novelty_windows,
|
|
488
|
+
"max_repeated_block": record.max_repeated_block,
|
|
489
|
+
"loop_evidence": record.loop_evidence,
|
|
490
|
+
"active_cycle": record.active_cycle,
|
|
491
|
+
"recovered_cycle": record.recovered_cycle,
|
|
492
|
+
"cycle_period_tokens": record.cycle_period_tokens,
|
|
493
|
+
"cycle_repetitions": record.cycle_repetitions,
|
|
494
|
+
"repeated_suffix_tokens": record.repeated_suffix_tokens,
|
|
495
|
+
"harness": record.harness,
|
|
496
|
+
"harness_version": record.harness_version,
|
|
497
|
+
"input_tokens": record.input_tokens,
|
|
498
|
+
"model_turns": record.model_turns,
|
|
499
|
+
"tool_calls": record.tool_calls,
|
|
500
|
+
"tool_trace": record.tool_trace,
|
|
501
|
+
"attempts": record.attempts,
|
|
502
|
+
"repair_attempts_used": record.repair_attempts_used,
|
|
503
|
+
"repair_feedback": record.repair_feedback,
|
|
504
|
+
"first_attempt_score": round(record.first_attempt_score * 100, 1),
|
|
505
|
+
"repaired": record.repaired,
|
|
506
|
+
"workspace": record.workspace or None,
|
|
507
|
+
"agentic_metrics": record.workspace.get("agentic_metrics") or None,
|
|
508
|
+
"decision": record.decision or None,
|
|
509
|
+
}
|
|
510
|
+
for record in records
|
|
511
|
+
],
|
|
512
|
+
**decision.summary([record.decision for record in scored_records], wall_time_s),
|
|
513
|
+
**pi_metadata,
|
|
514
|
+
**_result_metadata(job),
|
|
515
|
+
}
|
|
516
|
+
task_statistics = getattr(bench, "task_statistics", None)
|
|
517
|
+
if callable(task_statistics):
|
|
518
|
+
result["task_statistics"] = task_statistics(records)
|
|
519
|
+
# Multiple choice: a repair is a second guess at a small option set,
|
|
520
|
+
# so the headline is the first attempt and the repair gain sits beside
|
|
521
|
+
# it. Code and open-answer suites keep the repaired score.
|
|
522
|
+
if (
|
|
523
|
+
job.repair_attempts
|
|
524
|
+
and tasks
|
|
525
|
+
and all(option_count(bench, task) for task in tasks)
|
|
526
|
+
):
|
|
527
|
+
result.update(
|
|
528
|
+
headline="first_attempt",
|
|
529
|
+
repaired_score=result["score"],
|
|
530
|
+
repaired_passed=result["passed"],
|
|
531
|
+
score=result["first_attempt_score"],
|
|
532
|
+
passed=sum(record.first_attempt_score >= 1.0 for record in scored_records),
|
|
533
|
+
)
|
|
534
|
+
summary_fields = getattr(bench, "summary_fields", None)
|
|
535
|
+
if callable(summary_fields):
|
|
536
|
+
result.update(summary_fields(scored_records))
|
|
537
|
+
return result
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _result_metadata(job: JobSpec) -> dict:
|
|
541
|
+
bench = benchmark(job.benchmark)
|
|
542
|
+
metadata = getattr(bench, "result_metadata", None)
|
|
543
|
+
value = metadata(job.variant) if callable(metadata) else None
|
|
544
|
+
result = dict(value) if isinstance(value, dict) else {}
|
|
545
|
+
result.update(
|
|
546
|
+
harness=job.harness,
|
|
547
|
+
harness_label=job.harness_label,
|
|
548
|
+
repair_attempts=job.repair_attempts,
|
|
549
|
+
)
|
|
550
|
+
if job.perturbation:
|
|
551
|
+
result.update(
|
|
552
|
+
perturbation=job.perturbation,
|
|
553
|
+
perturbation_seed=job.perturbation_seed,
|
|
554
|
+
include_in_overall=False,
|
|
555
|
+
)
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def empty_result(job: JobSpec) -> dict:
|
|
560
|
+
"""Result shape for a job that finished without scoring a task."""
|
|
561
|
+
return {
|
|
562
|
+
"model": job.model,
|
|
563
|
+
"benchmark": job.benchmark,
|
|
564
|
+
"benchmark_label": job.benchmark_label,
|
|
565
|
+
"variant": job.variant,
|
|
566
|
+
"harness": job.harness,
|
|
567
|
+
"harness_label": job.harness_label,
|
|
568
|
+
"score": 0.0,
|
|
569
|
+
"passed": 0,
|
|
570
|
+
"total": 0,
|
|
571
|
+
"scored_total": 0,
|
|
572
|
+
"tok_s": 0.0,
|
|
573
|
+
"tok_s_per_stream": 0.0,
|
|
574
|
+
"tok_s_aggregate": 0.0,
|
|
575
|
+
"concurrency_eff": 0.0,
|
|
576
|
+
"throughput_coverage": 0.0,
|
|
577
|
+
"throughput_items": 0,
|
|
578
|
+
"throughput_wall_time": 0.0,
|
|
579
|
+
"total_output_tokens": 0,
|
|
580
|
+
"total_input_tokens": 0,
|
|
581
|
+
"model_turns": 0,
|
|
582
|
+
"tool_calls": 0,
|
|
583
|
+
"first_attempt_score": 0.0,
|
|
584
|
+
"repair_delta_pp": 0.0,
|
|
585
|
+
"repair_attempted": 0,
|
|
586
|
+
"repair_successes": 0,
|
|
587
|
+
"sum_generation_time": 0.0,
|
|
588
|
+
"sum_request_time": 0.0,
|
|
589
|
+
"avg_response_time": 0.0,
|
|
590
|
+
"total_time": 0.0,
|
|
591
|
+
"slice": job.slice_spec,
|
|
592
|
+
"concurrency": 1,
|
|
593
|
+
"errors": 0,
|
|
594
|
+
"harness_errors": 0,
|
|
595
|
+
"contaminated": 0,
|
|
596
|
+
"contamination_by_language": {},
|
|
597
|
+
"contaminated_tasks": [],
|
|
598
|
+
"length_exceeded": 0,
|
|
599
|
+
"timeouts": 0,
|
|
600
|
+
"loop_kills": 0,
|
|
601
|
+
"failures": 0,
|
|
602
|
+
"loops": 0,
|
|
603
|
+
"suspected_loops": 0,
|
|
604
|
+
"loop_rate": 0.0,
|
|
605
|
+
"trace_coverage": 0.0,
|
|
606
|
+
"median_thinking_time": 0.0,
|
|
607
|
+
"median_time_to_answer": 0.0,
|
|
608
|
+
"tasks": [],
|
|
609
|
+
**_result_metadata(job),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _harness_pair_key(result: dict) -> tuple[object, ...]:
|
|
614
|
+
return (
|
|
615
|
+
result.get("model"),
|
|
616
|
+
result.get("benchmark"),
|
|
617
|
+
result.get("variant"),
|
|
618
|
+
result.get("slice"),
|
|
619
|
+
result.get("perturbation"),
|
|
620
|
+
result.get("perturbation_seed") if result.get("perturbation") else None,
|
|
621
|
+
result.get("repair_attempts", 0),
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def annotate_harness_effect(results: list[dict]) -> None:
|
|
626
|
+
"""Attach paired direct-versus-agent deltas to Pi result rows."""
|
|
627
|
+
direct = {
|
|
628
|
+
_harness_pair_key(result): result
|
|
629
|
+
for result in results
|
|
630
|
+
if result.get("harness", "direct") == "direct"
|
|
631
|
+
}
|
|
632
|
+
for result in results:
|
|
633
|
+
if result.get("harness") != "pi":
|
|
634
|
+
continue
|
|
635
|
+
baseline = direct.get(_harness_pair_key(result))
|
|
636
|
+
if baseline is None:
|
|
637
|
+
continue
|
|
638
|
+
baseline_tasks = {task["task_id"]: task for task in baseline.get("tasks", [])}
|
|
639
|
+
pairs = [
|
|
640
|
+
(baseline_tasks[task["task_id"]], task)
|
|
641
|
+
for task in result.get("tasks", [])
|
|
642
|
+
if task["task_id"] in baseline_tasks
|
|
643
|
+
and not task.get("harness_error")
|
|
644
|
+
and not baseline_tasks[task["task_id"]].get("harness_error")
|
|
645
|
+
]
|
|
646
|
+
if not pairs:
|
|
647
|
+
continue
|
|
648
|
+
total = len(pairs)
|
|
649
|
+
direct_points = sum(float(before.get("score", 0)) for before, _ in pairs)
|
|
650
|
+
pi_points = sum(float(after.get("score", 0)) for _, after in pairs)
|
|
651
|
+
direct_first_points = sum(
|
|
652
|
+
float(before.get("first_attempt_score", before.get("score", 0)))
|
|
653
|
+
for before, _ in pairs
|
|
654
|
+
)
|
|
655
|
+
pi_first_points = sum(
|
|
656
|
+
float(after.get("first_attempt_score", after.get("score", 0)))
|
|
657
|
+
for _, after in pairs
|
|
658
|
+
)
|
|
659
|
+
gains = sum(
|
|
660
|
+
not bool(before.get("passed")) and bool(after.get("passed"))
|
|
661
|
+
for before, after in pairs
|
|
662
|
+
)
|
|
663
|
+
regressions = sum(
|
|
664
|
+
bool(before.get("passed")) and not bool(after.get("passed"))
|
|
665
|
+
for before, after in pairs
|
|
666
|
+
)
|
|
667
|
+
direct_loop_kills = sum(bool(before.get("loop_killed")) for before, _ in pairs)
|
|
668
|
+
harness_loop_kills = sum(bool(after.get("loop_killed")) for _, after in pairs)
|
|
669
|
+
result.update(
|
|
670
|
+
harness_paired_total=total,
|
|
671
|
+
direct_score=round(direct_points / total, 1),
|
|
672
|
+
harness_score=round(pi_points / total, 1),
|
|
673
|
+
harness_score_delta_pp=round((pi_points - direct_points) / total, 1),
|
|
674
|
+
direct_first_score=round(direct_first_points / total, 1),
|
|
675
|
+
harness_first_score=round(pi_first_points / total, 1),
|
|
676
|
+
harness_first_score_delta_pp=round(
|
|
677
|
+
(pi_first_points - direct_first_points) / total,
|
|
678
|
+
1,
|
|
679
|
+
),
|
|
680
|
+
harness_gains=gains,
|
|
681
|
+
harness_regressions=regressions,
|
|
682
|
+
direct_loop_kill_rate=round(direct_loop_kills / total * 100, 1),
|
|
683
|
+
harness_loop_kill_rate=round(harness_loop_kills / total * 100, 1),
|
|
684
|
+
loop_kill_delta_pp=round(
|
|
685
|
+
(harness_loop_kills - direct_loop_kills) / total * 100,
|
|
686
|
+
1,
|
|
687
|
+
),
|
|
688
|
+
)
|