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/pi_agent.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""Stock Pi coding-agent RPC adapter running inside a Docker task sandbox."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import queue
|
|
9
|
+
import subprocess
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from benchkit.client import GenerationUpdate, InferenceClient
|
|
18
|
+
from benchkit.evaluation import EvaluationResult, combine_attempts, repair_message
|
|
19
|
+
from benchkit.sandbox import (
|
|
20
|
+
DockerTaskEnvironment,
|
|
21
|
+
LatestPiImage,
|
|
22
|
+
cleanup_owned_resources,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _message_blocks(message: object, kind: str, field: str) -> str:
|
|
27
|
+
if not isinstance(message, dict):
|
|
28
|
+
return ""
|
|
29
|
+
content = message.get("content")
|
|
30
|
+
if not isinstance(content, list):
|
|
31
|
+
return ""
|
|
32
|
+
return "".join(
|
|
33
|
+
str(block.get(field) or "")
|
|
34
|
+
for block in content
|
|
35
|
+
if isinstance(block, dict) and block.get("type") == kind
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _RpcTrace:
|
|
41
|
+
"""Incrementally normalize Pi events into BenchKit's generation shape."""
|
|
42
|
+
|
|
43
|
+
final_response: str = ""
|
|
44
|
+
thinking_parts: list[str] = field(default_factory=list)
|
|
45
|
+
output_tokens: int = 0
|
|
46
|
+
input_tokens: int = 0
|
|
47
|
+
turns: int = 0
|
|
48
|
+
tool_calls_started: int = 0
|
|
49
|
+
tool_trace: list[dict[str, Any]] = field(default_factory=list)
|
|
50
|
+
done_reason: str = "stop"
|
|
51
|
+
generation_time_s: float = 0.0
|
|
52
|
+
_assistant_started_at: float | None = field(default=None, repr=False)
|
|
53
|
+
_active_tools: dict[str, tuple[float, dict[str, Any]]] = field(
|
|
54
|
+
default_factory=dict, repr=False
|
|
55
|
+
)
|
|
56
|
+
_streamed_thinking: list[str] = field(default_factory=list, repr=False)
|
|
57
|
+
_streamed_text: list[str] = field(default_factory=list, repr=False)
|
|
58
|
+
|
|
59
|
+
def handle(self, event: dict[str, Any]) -> tuple[str, str]:
|
|
60
|
+
"""Consume one RPC event and return streamed thinking/text deltas."""
|
|
61
|
+
event_type = event.get("type")
|
|
62
|
+
thinking_delta = ""
|
|
63
|
+
text_delta = ""
|
|
64
|
+
if event_type == "message_start":
|
|
65
|
+
message = event.get("message") or {}
|
|
66
|
+
if message.get("role") == "assistant":
|
|
67
|
+
self._assistant_started_at = time.monotonic()
|
|
68
|
+
self._streamed_thinking.clear()
|
|
69
|
+
self._streamed_text.clear()
|
|
70
|
+
elif event_type == "message_update":
|
|
71
|
+
delta = event.get("assistantMessageEvent") or {}
|
|
72
|
+
if delta.get("type") == "thinking_delta":
|
|
73
|
+
thinking_delta = str(delta.get("delta") or "")
|
|
74
|
+
self._streamed_thinking.append(thinking_delta)
|
|
75
|
+
elif delta.get("type") == "text_delta":
|
|
76
|
+
text_delta = str(delta.get("delta") or "")
|
|
77
|
+
self._streamed_text.append(text_delta)
|
|
78
|
+
if (thinking_delta or text_delta) and self._assistant_started_at is None:
|
|
79
|
+
self._assistant_started_at = time.monotonic()
|
|
80
|
+
elif event_type == "message_end":
|
|
81
|
+
message = event.get("message") or {}
|
|
82
|
+
if message.get("role") == "assistant":
|
|
83
|
+
if self._assistant_started_at is not None:
|
|
84
|
+
self.generation_time_s += max(
|
|
85
|
+
0.0, time.monotonic() - self._assistant_started_at
|
|
86
|
+
)
|
|
87
|
+
self._assistant_started_at = None
|
|
88
|
+
self.turns += 1
|
|
89
|
+
text = _message_blocks(message, "text", "text") or "".join(
|
|
90
|
+
self._streamed_text
|
|
91
|
+
)
|
|
92
|
+
if text:
|
|
93
|
+
self.final_response = text
|
|
94
|
+
thinking = _message_blocks(message, "thinking", "thinking") or "".join(
|
|
95
|
+
self._streamed_thinking
|
|
96
|
+
)
|
|
97
|
+
if thinking:
|
|
98
|
+
self.thinking_parts.append(thinking)
|
|
99
|
+
usage = message.get("usage") or {}
|
|
100
|
+
self.input_tokens += sum(
|
|
101
|
+
int(usage.get(key) or 0)
|
|
102
|
+
for key in ("input", "cacheRead", "cacheWrite")
|
|
103
|
+
)
|
|
104
|
+
self.output_tokens += int(usage.get("output") or 0)
|
|
105
|
+
self.done_reason = str(message.get("stopReason") or self.done_reason)
|
|
106
|
+
self._streamed_thinking.clear()
|
|
107
|
+
self._streamed_text.clear()
|
|
108
|
+
elif event_type == "tool_execution_start":
|
|
109
|
+
self.tool_calls_started += 1
|
|
110
|
+
tool_id = str(event.get("toolCallId") or "")
|
|
111
|
+
item = {
|
|
112
|
+
"name": str(event.get("toolName") or ""),
|
|
113
|
+
"arguments": event.get("args") or {},
|
|
114
|
+
}
|
|
115
|
+
self._active_tools[tool_id] = (time.monotonic(), item)
|
|
116
|
+
elif event_type == "tool_execution_end":
|
|
117
|
+
tool_id = str(event.get("toolCallId") or "")
|
|
118
|
+
started, item = self._active_tools.pop(
|
|
119
|
+
tool_id,
|
|
120
|
+
(time.monotonic(), {"name": str(event.get("toolName") or "")}),
|
|
121
|
+
)
|
|
122
|
+
item.update(
|
|
123
|
+
is_error=bool(event.get("isError")),
|
|
124
|
+
duration_s=round(time.monotonic() - started, 3),
|
|
125
|
+
)
|
|
126
|
+
output = _message_blocks(event.get("result"), "text", "text")
|
|
127
|
+
if output:
|
|
128
|
+
item["output_truncated"] = len(output) > 8000
|
|
129
|
+
item["output"] = output[-8000:]
|
|
130
|
+
self.tool_trace.append(item)
|
|
131
|
+
return thinking_delta, text_delta
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class PiAgentRunner:
|
|
136
|
+
"""Run the original latest Pi agent, without replacing any native tools."""
|
|
137
|
+
|
|
138
|
+
client: InferenceClient
|
|
139
|
+
image: LatestPiImage = field(default_factory=LatestPiImage)
|
|
140
|
+
owner_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def version(self) -> str:
|
|
144
|
+
return self.image.version
|
|
145
|
+
|
|
146
|
+
def prepare(self) -> str:
|
|
147
|
+
return self.image.prepare()
|
|
148
|
+
|
|
149
|
+
def cleanup(self) -> None:
|
|
150
|
+
try:
|
|
151
|
+
cleanup_owned_resources(self.image.docker, self.owner_id)
|
|
152
|
+
finally:
|
|
153
|
+
self.image.cleanup()
|
|
154
|
+
|
|
155
|
+
def generate(
|
|
156
|
+
self,
|
|
157
|
+
model: str,
|
|
158
|
+
prompt: str,
|
|
159
|
+
on_progress=None,
|
|
160
|
+
cancel_event: threading.Event | None = None,
|
|
161
|
+
verifier: Callable[[str], EvaluationResult] | None = None,
|
|
162
|
+
repair_attempts: int = 0,
|
|
163
|
+
workspace_setup: Callable[[DockerTaskEnvironment], None] | None = None,
|
|
164
|
+
workspace_verifier: Callable[
|
|
165
|
+
[DockerTaskEnvironment, list[dict[str, Any]]], EvaluationResult
|
|
166
|
+
]
|
|
167
|
+
| None = None,
|
|
168
|
+
repair_prompt_builder: Callable[[str, int, int], str] | None = None,
|
|
169
|
+
) -> dict:
|
|
170
|
+
self.prepare()
|
|
171
|
+
started = time.perf_counter()
|
|
172
|
+
try:
|
|
173
|
+
timeout = max(0.1, float(os.environ.get("BENCHKIT_PI_TIMEOUT", "900")))
|
|
174
|
+
except ValueError:
|
|
175
|
+
timeout = 900.0
|
|
176
|
+
stderr_parts: list[str] = []
|
|
177
|
+
events: queue.Queue[dict[str, Any] | None] = queue.Queue()
|
|
178
|
+
process: subprocess.Popen[str] | None = None
|
|
179
|
+
timed_out = False
|
|
180
|
+
cancelled = False
|
|
181
|
+
generation_results: list[dict] = []
|
|
182
|
+
attempts: list[tuple[dict, EvaluationResult]] = []
|
|
183
|
+
feedback_sent: list[str] = []
|
|
184
|
+
previous_stats = (0, 0, 0)
|
|
185
|
+
verification_time = 0.0
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with DockerTaskEnvironment(
|
|
189
|
+
self.client,
|
|
190
|
+
model,
|
|
191
|
+
self.image,
|
|
192
|
+
owner_id=self.owner_id,
|
|
193
|
+
) as environment:
|
|
194
|
+
if workspace_setup is not None:
|
|
195
|
+
workspace_setup(environment)
|
|
196
|
+
process = environment.start_pi()
|
|
197
|
+
assert process.stdin is not None
|
|
198
|
+
assert process.stdout is not None
|
|
199
|
+
assert process.stderr is not None
|
|
200
|
+
|
|
201
|
+
def read_stdout() -> None:
|
|
202
|
+
for line in process.stdout:
|
|
203
|
+
try:
|
|
204
|
+
events.put(json.loads(line))
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
continue
|
|
207
|
+
events.put(None)
|
|
208
|
+
|
|
209
|
+
def read_stderr() -> None:
|
|
210
|
+
for line in process.stderr:
|
|
211
|
+
stderr_parts.append(line)
|
|
212
|
+
if sum(map(len, stderr_parts)) > 16000:
|
|
213
|
+
del stderr_parts[: len(stderr_parts) // 2]
|
|
214
|
+
|
|
215
|
+
threading.Thread(target=read_stdout, daemon=True).start()
|
|
216
|
+
threading.Thread(target=read_stderr, daemon=True).start()
|
|
217
|
+
|
|
218
|
+
send_lock = threading.Lock()
|
|
219
|
+
|
|
220
|
+
def send(payload: dict[str, Any]) -> None:
|
|
221
|
+
with send_lock:
|
|
222
|
+
if process is None or process.stdin is None:
|
|
223
|
+
return
|
|
224
|
+
try:
|
|
225
|
+
process.stdin.write(json.dumps(payload) + "\n")
|
|
226
|
+
process.stdin.flush()
|
|
227
|
+
except OSError:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
message = prompt
|
|
231
|
+
for attempt in range(max(0, repair_attempts) + 1):
|
|
232
|
+
trace = _RpcTrace()
|
|
233
|
+
turn_started = time.perf_counter()
|
|
234
|
+
prompt_id = f"benchkit-prompt-{attempt}"
|
|
235
|
+
stats_id = f"benchkit-stats-{attempt}"
|
|
236
|
+
final_id = f"benchkit-final-{attempt}"
|
|
237
|
+
send({"id": prompt_id, "type": "prompt", "message": message})
|
|
238
|
+
requested_summary = False
|
|
239
|
+
summary_responses: set[str] = set()
|
|
240
|
+
stats: dict[str, Any] = {}
|
|
241
|
+
last_text: str | None = None
|
|
242
|
+
|
|
243
|
+
while True:
|
|
244
|
+
elapsed = time.perf_counter() - started
|
|
245
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
246
|
+
cancelled = True
|
|
247
|
+
send({"type": "abort"})
|
|
248
|
+
break
|
|
249
|
+
if elapsed >= timeout:
|
|
250
|
+
timed_out = True
|
|
251
|
+
send({"type": "abort"})
|
|
252
|
+
break
|
|
253
|
+
try:
|
|
254
|
+
event = events.get(timeout=0.1)
|
|
255
|
+
except queue.Empty:
|
|
256
|
+
continue
|
|
257
|
+
if event is None:
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
thinking_delta, text_delta = trace.handle(event)
|
|
261
|
+
if on_progress is not None and (thinking_delta or text_delta):
|
|
262
|
+
on_progress(
|
|
263
|
+
GenerationUpdate(
|
|
264
|
+
thinking=thinking_delta,
|
|
265
|
+
response=text_delta,
|
|
266
|
+
elapsed_s=elapsed,
|
|
267
|
+
reasoning_channel_seen=bool(
|
|
268
|
+
thinking_delta or trace.thinking_parts
|
|
269
|
+
),
|
|
270
|
+
attempt=attempt,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
event.get("type") == "agent_settled"
|
|
276
|
+
and not requested_summary
|
|
277
|
+
):
|
|
278
|
+
requested_summary = True
|
|
279
|
+
send({"id": stats_id, "type": "get_session_stats"})
|
|
280
|
+
send(
|
|
281
|
+
{
|
|
282
|
+
"id": final_id,
|
|
283
|
+
"type": "get_last_assistant_text",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
continue
|
|
287
|
+
if event.get("type") == "response":
|
|
288
|
+
event_id = event.get("id")
|
|
289
|
+
if event_id == prompt_id and not event.get("success"):
|
|
290
|
+
detail = event.get("error") or "Pi rejected the prompt"
|
|
291
|
+
raise RuntimeError(str(detail))
|
|
292
|
+
data = event.get("data") or {}
|
|
293
|
+
if event_id == stats_id:
|
|
294
|
+
stats = data
|
|
295
|
+
summary_responses.add("stats")
|
|
296
|
+
elif event_id == final_id:
|
|
297
|
+
value = data.get("text")
|
|
298
|
+
last_text = str(value) if value is not None else None
|
|
299
|
+
summary_responses.add("final")
|
|
300
|
+
if requested_summary and summary_responses == {
|
|
301
|
+
"stats",
|
|
302
|
+
"final",
|
|
303
|
+
}:
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
if last_text:
|
|
307
|
+
trace.final_response = last_text
|
|
308
|
+
tokens = stats.get("tokens") or {}
|
|
309
|
+
if tokens:
|
|
310
|
+
current_stats = (
|
|
311
|
+
sum(
|
|
312
|
+
int(tokens.get(key) or 0)
|
|
313
|
+
for key in ("input", "cacheRead", "cacheWrite")
|
|
314
|
+
),
|
|
315
|
+
int(tokens.get("output") or 0),
|
|
316
|
+
int(stats.get("assistantMessages") or 0),
|
|
317
|
+
)
|
|
318
|
+
trace.input_tokens = max(
|
|
319
|
+
trace.input_tokens,
|
|
320
|
+
current_stats[0] - previous_stats[0],
|
|
321
|
+
)
|
|
322
|
+
trace.output_tokens = max(
|
|
323
|
+
trace.output_tokens,
|
|
324
|
+
current_stats[1] - previous_stats[1],
|
|
325
|
+
)
|
|
326
|
+
trace.turns = max(
|
|
327
|
+
trace.turns,
|
|
328
|
+
current_stats[2] - previous_stats[2],
|
|
329
|
+
)
|
|
330
|
+
previous_stats = current_stats
|
|
331
|
+
|
|
332
|
+
turn_elapsed = time.perf_counter() - turn_started
|
|
333
|
+
if on_progress is not None and not cancelled:
|
|
334
|
+
on_progress(
|
|
335
|
+
GenerationUpdate(
|
|
336
|
+
elapsed_s=time.perf_counter() - started,
|
|
337
|
+
reasoning_channel_seen=bool(trace.thinking_parts),
|
|
338
|
+
done=True,
|
|
339
|
+
attempt=attempt,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
thinking = "\n\n".join(trace.thinking_parts)
|
|
343
|
+
error = "".join(stderr_parts).strip()
|
|
344
|
+
length_exceeded = trace.done_reason == "length"
|
|
345
|
+
if (
|
|
346
|
+
not trace.final_response
|
|
347
|
+
and not (cancelled or timed_out or length_exceeded)
|
|
348
|
+
and trace.turns == 0
|
|
349
|
+
):
|
|
350
|
+
detail = (
|
|
351
|
+
error[-2000:]
|
|
352
|
+
if error
|
|
353
|
+
else "no assistant message was returned"
|
|
354
|
+
)
|
|
355
|
+
raise RuntimeError(
|
|
356
|
+
f"Pi agent exited without a response: {detail}"
|
|
357
|
+
)
|
|
358
|
+
output_tokens = trace.output_tokens
|
|
359
|
+
generation_time_s = trace.generation_time_s
|
|
360
|
+
generation = {
|
|
361
|
+
"thinking": thinking,
|
|
362
|
+
"response": trace.final_response,
|
|
363
|
+
"trace_status": (
|
|
364
|
+
"observed"
|
|
365
|
+
if thinking
|
|
366
|
+
else "available_empty"
|
|
367
|
+
if trace.turns
|
|
368
|
+
else "unavailable"
|
|
369
|
+
),
|
|
370
|
+
"tok_s": (
|
|
371
|
+
output_tokens / generation_time_s
|
|
372
|
+
if generation_time_s > 0
|
|
373
|
+
else 0.0
|
|
374
|
+
),
|
|
375
|
+
"eval_count": output_tokens,
|
|
376
|
+
"eval_duration_ns": int(generation_time_s * 1e9),
|
|
377
|
+
"response_time_s": turn_elapsed,
|
|
378
|
+
"done_reason": (
|
|
379
|
+
"cancelled"
|
|
380
|
+
if cancelled
|
|
381
|
+
else "timeout"
|
|
382
|
+
if timed_out
|
|
383
|
+
else trace.done_reason
|
|
384
|
+
),
|
|
385
|
+
"timed_out": timed_out,
|
|
386
|
+
"timeout_s": timeout,
|
|
387
|
+
"cancelled": cancelled,
|
|
388
|
+
"length_exceeded": length_exceeded,
|
|
389
|
+
"harness": "pi",
|
|
390
|
+
"harness_version": self.version,
|
|
391
|
+
"input_tokens": trace.input_tokens,
|
|
392
|
+
"model_turns": trace.turns,
|
|
393
|
+
"tool_calls": trace.tool_calls_started,
|
|
394
|
+
"tool_trace": trace.tool_trace,
|
|
395
|
+
"workspace_evaluated": workspace_verifier is not None,
|
|
396
|
+
}
|
|
397
|
+
generation_results.append(generation)
|
|
398
|
+
|
|
399
|
+
active_verifier = (
|
|
400
|
+
(
|
|
401
|
+
lambda _response, current_trace=trace: workspace_verifier(
|
|
402
|
+
environment, current_trace.tool_trace
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
if workspace_verifier is not None
|
|
406
|
+
else verifier
|
|
407
|
+
)
|
|
408
|
+
if active_verifier is None:
|
|
409
|
+
break
|
|
410
|
+
terminal = cancelled or timed_out or length_exceeded
|
|
411
|
+
verification_started = time.perf_counter()
|
|
412
|
+
evaluation = (
|
|
413
|
+
EvaluationResult(score=0.0)
|
|
414
|
+
if cancelled or (terminal and workspace_verifier is None)
|
|
415
|
+
else active_verifier(trace.final_response)
|
|
416
|
+
)
|
|
417
|
+
verification_time += time.perf_counter() - verification_started
|
|
418
|
+
attempts.append((generation, evaluation))
|
|
419
|
+
if (
|
|
420
|
+
terminal
|
|
421
|
+
or evaluation.passed
|
|
422
|
+
or evaluation.error
|
|
423
|
+
or evaluation.details.get("contamination_verdict")
|
|
424
|
+
== "CONTAMINATED"
|
|
425
|
+
):
|
|
426
|
+
break
|
|
427
|
+
if attempt >= repair_attempts:
|
|
428
|
+
break
|
|
429
|
+
feedback_sent.append(evaluation.feedback)
|
|
430
|
+
builder = repair_prompt_builder or repair_message
|
|
431
|
+
message = builder(evaluation.feedback, attempt + 1, repair_attempts)
|
|
432
|
+
scaffold_value = environment.pi_scaffold()
|
|
433
|
+
scaffold = (
|
|
434
|
+
dict(scaffold_value) if isinstance(scaffold_value, dict) else {}
|
|
435
|
+
)
|
|
436
|
+
system_prompt = str(scaffold.get("system_prompt") or "")
|
|
437
|
+
tokenize = getattr(self.client, "tokenize", None)
|
|
438
|
+
if system_prompt and callable(tokenize):
|
|
439
|
+
try:
|
|
440
|
+
tokens = tokenize(model, system_prompt, add_special=False)
|
|
441
|
+
except (TypeError, ValueError):
|
|
442
|
+
tokens = None
|
|
443
|
+
if tokens is not None:
|
|
444
|
+
scaffold["system_prompt_tokens"] = len(tokens)
|
|
445
|
+
for generation in generation_results:
|
|
446
|
+
generation["pi_scaffold"] = scaffold
|
|
447
|
+
finally:
|
|
448
|
+
if process is not None and process.poll() is None:
|
|
449
|
+
process.terminate()
|
|
450
|
+
try:
|
|
451
|
+
process.wait(timeout=3)
|
|
452
|
+
except subprocess.TimeoutExpired:
|
|
453
|
+
process.kill()
|
|
454
|
+
with contextlib.suppress(subprocess.TimeoutExpired):
|
|
455
|
+
process.wait(timeout=3)
|
|
456
|
+
|
|
457
|
+
if not generation_results:
|
|
458
|
+
raise RuntimeError("Pi agent exited before producing a generation")
|
|
459
|
+
|
|
460
|
+
elapsed = time.perf_counter() - started
|
|
461
|
+
accounted = sum(
|
|
462
|
+
float(generation.get("response_time_s") or 0.0)
|
|
463
|
+
for generation in generation_results
|
|
464
|
+
)
|
|
465
|
+
overhead = max(0.0, elapsed - accounted - verification_time)
|
|
466
|
+
last_generation = generation_results[-1]
|
|
467
|
+
last_generation["response_time_s"] += overhead
|
|
468
|
+
|
|
469
|
+
if verifier is None and workspace_verifier is None:
|
|
470
|
+
output_tokens = int(last_generation.get("eval_count") or 0)
|
|
471
|
+
generation_ns = int(last_generation.get("eval_duration_ns") or 0)
|
|
472
|
+
last_generation["tok_s"] = (
|
|
473
|
+
output_tokens / (generation_ns / 1e9) if generation_ns > 0 else 0.0
|
|
474
|
+
)
|
|
475
|
+
return last_generation
|
|
476
|
+
return combine_attempts(attempts, feedback_sent)
|