agent-skill-eval 0.2.1__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.
- agent_skill_eval-0.2.1.dist-info/METADATA +335 -0
- agent_skill_eval-0.2.1.dist-info/RECORD +15 -0
- agent_skill_eval-0.2.1.dist-info/WHEEL +4 -0
- agent_skill_eval-0.2.1.dist-info/entry_points.txt +2 -0
- agent_skill_eval-0.2.1.dist-info/licenses/LICENSE +21 -0
- skill_eval/__init__.py +8 -0
- skill_eval/__main__.py +3 -0
- skill_eval/cli.py +844 -0
- skill_eval/git_state.py +227 -0
- skill_eval/graders/__init__.py +810 -0
- skill_eval/harnesses/__init__.py +337 -0
- skill_eval/models.py +146 -0
- skill_eval/runner.py +424 -0
- skill_eval/skills.py +88 -0
- skill_eval/workspace.py +101 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from skill_eval.models import AgentType, TimingData
|
|
12
|
+
|
|
13
|
+
DEFAULT_TIMEOUT_SECONDS = 600
|
|
14
|
+
DEFAULT_MAX_RETRIES = 1
|
|
15
|
+
RETRY_BACKOFF_SECONDS = 2.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentHarness(ABC):
|
|
19
|
+
agent_type: AgentType
|
|
20
|
+
# Environment variable each agent CLI reads its API base URL from.
|
|
21
|
+
base_url_env: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
workspace: Path,
|
|
26
|
+
model: Optional[str] = None,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
timeout: Optional[int] = None,
|
|
29
|
+
max_retries: Optional[int] = None,
|
|
30
|
+
):
|
|
31
|
+
self.workspace = workspace
|
|
32
|
+
self.model = model
|
|
33
|
+
self.base_url = base_url
|
|
34
|
+
self.timeout = timeout or int(os.environ.get("SKILL_EVAL_AGENT_TIMEOUT", DEFAULT_TIMEOUT_SECONDS))
|
|
35
|
+
if max_retries is None:
|
|
36
|
+
max_retries = int(os.environ.get("SKILL_EVAL_AGENT_RETRIES", DEFAULT_MAX_RETRIES))
|
|
37
|
+
self.max_retries = max_retries
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def build_command(self, prompt: str, output_dir: Path) -> list[str]:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def parse_output(self, stdout: str, stderr: str) -> tuple[str, TimingData]:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def _build_env(self) -> Optional[dict[str, str]]:
|
|
48
|
+
if self.base_url and self.base_url_env:
|
|
49
|
+
env = dict(os.environ)
|
|
50
|
+
env[self.base_url_env] = self.base_url
|
|
51
|
+
return env
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def _workspace_fingerprint(self) -> Optional[tuple[str, str]]:
|
|
55
|
+
"""(HEAD sha, porcelain status) of the workspace, or None outside git.
|
|
56
|
+
|
|
57
|
+
Used to refuse retries after a failed attempt that already mutated
|
|
58
|
+
the workspace — re-running there would grade the union of both
|
|
59
|
+
attempts' side effects.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
head = subprocess.run(
|
|
63
|
+
["git", "rev-parse", "HEAD"],
|
|
64
|
+
cwd=self.workspace,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
encoding="utf-8",
|
|
68
|
+
timeout=15,
|
|
69
|
+
check=False,
|
|
70
|
+
)
|
|
71
|
+
status = subprocess.run(
|
|
72
|
+
["git", "status", "--porcelain"],
|
|
73
|
+
cwd=self.workspace,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
timeout=15,
|
|
78
|
+
check=False,
|
|
79
|
+
)
|
|
80
|
+
except (subprocess.SubprocessError, OSError):
|
|
81
|
+
return None
|
|
82
|
+
if head.returncode != 0 or status.returncode != 0:
|
|
83
|
+
return None
|
|
84
|
+
return (head.stdout.strip(), status.stdout)
|
|
85
|
+
|
|
86
|
+
def run(self, prompt: str, output_dir: Path) -> tuple[str, TimingData, str, str]:
|
|
87
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
cmd = self.build_command(prompt, output_dir)
|
|
90
|
+
env = self._build_env()
|
|
91
|
+
|
|
92
|
+
attempts = self.max_retries + 1
|
|
93
|
+
stdout = ""
|
|
94
|
+
stderr = ""
|
|
95
|
+
exit_code: Optional[int] = None
|
|
96
|
+
timed_out = False
|
|
97
|
+
retries_used = 0
|
|
98
|
+
start = time.time()
|
|
99
|
+
fingerprint_before = self._workspace_fingerprint()
|
|
100
|
+
|
|
101
|
+
for attempt in range(attempts):
|
|
102
|
+
if attempt > 0:
|
|
103
|
+
retries_used = attempt
|
|
104
|
+
time.sleep(RETRY_BACKOFF_SECONDS * attempt)
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
cmd,
|
|
108
|
+
cwd=self.workspace,
|
|
109
|
+
capture_output=True,
|
|
110
|
+
text=True,
|
|
111
|
+
encoding="utf-8",
|
|
112
|
+
timeout=self.timeout,
|
|
113
|
+
env=env,
|
|
114
|
+
)
|
|
115
|
+
stdout = result.stdout or ""
|
|
116
|
+
stderr = result.stderr or ""
|
|
117
|
+
exit_code = result.returncode
|
|
118
|
+
timed_out = False
|
|
119
|
+
if result.returncode == 0:
|
|
120
|
+
break
|
|
121
|
+
except subprocess.TimeoutExpired as e:
|
|
122
|
+
stdout = (e.stdout or b"").decode() if isinstance(e.stdout, bytes) else (e.stdout or "")
|
|
123
|
+
stderr = (e.stderr or b"").decode() if isinstance(e.stderr, bytes) else (e.stderr or "")
|
|
124
|
+
stderr += f"\n[skill-eval] agent timed out after {self.timeout}s (attempt {attempt + 1}/{attempts})"
|
|
125
|
+
exit_code = None
|
|
126
|
+
timed_out = True
|
|
127
|
+
|
|
128
|
+
# Only retry into a pristine workspace: if the failed attempt
|
|
129
|
+
# already committed/wrote files, a second run would produce a
|
|
130
|
+
# graded union of both attempts.
|
|
131
|
+
if attempt < attempts - 1 and fingerprint_before is not None:
|
|
132
|
+
if self._workspace_fingerprint() != fingerprint_before:
|
|
133
|
+
stderr += "\n[skill-eval] not retrying: the failed attempt modified the workspace"
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
137
|
+
|
|
138
|
+
final_output, timing = self.parse_output(stdout, stderr)
|
|
139
|
+
if timing.duration_ms == 0:
|
|
140
|
+
timing.duration_ms = duration_ms
|
|
141
|
+
timing.exit_code = exit_code
|
|
142
|
+
timing.timed_out = timed_out
|
|
143
|
+
timing.retries = retries_used
|
|
144
|
+
|
|
145
|
+
with open(output_dir / "stdout.log", "w", encoding="utf-8") as f:
|
|
146
|
+
f.write(stdout)
|
|
147
|
+
with open(output_dir / "stderr.log", "w", encoding="utf-8") as f:
|
|
148
|
+
f.write(stderr)
|
|
149
|
+
|
|
150
|
+
return final_output, timing, stdout, stderr
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class OpenCodeHarness(AgentHarness):
|
|
154
|
+
agent_type = AgentType.OPENCODE
|
|
155
|
+
base_url_env = "OPENAI_BASE_URL"
|
|
156
|
+
|
|
157
|
+
def build_command(self, prompt: str, output_dir: Path) -> list[str]:
|
|
158
|
+
cmd = [
|
|
159
|
+
"opencode",
|
|
160
|
+
"run",
|
|
161
|
+
"--dir",
|
|
162
|
+
str(self.workspace),
|
|
163
|
+
"--format",
|
|
164
|
+
"json",
|
|
165
|
+
"--dangerously-skip-permissions",
|
|
166
|
+
]
|
|
167
|
+
if self.model:
|
|
168
|
+
cmd.extend(["--model", self.model])
|
|
169
|
+
cmd.append(prompt)
|
|
170
|
+
return cmd
|
|
171
|
+
|
|
172
|
+
def parse_output(self, stdout: str, stderr: str) -> tuple[str, TimingData]:
|
|
173
|
+
timing = TimingData()
|
|
174
|
+
messages = []
|
|
175
|
+
|
|
176
|
+
for line in stdout.strip().split("\n"):
|
|
177
|
+
if not line.strip():
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
event = json.loads(line)
|
|
181
|
+
etype = event.get("type", "")
|
|
182
|
+
|
|
183
|
+
if etype == "text":
|
|
184
|
+
part = event.get("part", {})
|
|
185
|
+
text = part.get("text", "")
|
|
186
|
+
if text:
|
|
187
|
+
messages.append(text)
|
|
188
|
+
|
|
189
|
+
elif etype == "step_finish":
|
|
190
|
+
part = event.get("part", {})
|
|
191
|
+
tokens = part.get("tokens", {})
|
|
192
|
+
timing.input_tokens += tokens.get("input", 0)
|
|
193
|
+
timing.output_tokens += tokens.get("output", 0)
|
|
194
|
+
cache = tokens.get("cache", {})
|
|
195
|
+
timing.cached_tokens += cache.get("read", 0)
|
|
196
|
+
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
timing.total_tokens = timing.input_tokens + timing.output_tokens
|
|
201
|
+
return "\n".join(messages), timing
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ClaudeCodeHarness(AgentHarness):
|
|
205
|
+
agent_type = AgentType.CLAUDE_CODE
|
|
206
|
+
base_url_env = "ANTHROPIC_BASE_URL"
|
|
207
|
+
|
|
208
|
+
def build_command(self, prompt: str, output_dir: Path) -> list[str]:
|
|
209
|
+
cmd = [
|
|
210
|
+
"claude",
|
|
211
|
+
"-p",
|
|
212
|
+
"--output-format",
|
|
213
|
+
"json",
|
|
214
|
+
"--dangerously-skip-permissions",
|
|
215
|
+
]
|
|
216
|
+
if self.model:
|
|
217
|
+
cmd.extend(["--model", self.model])
|
|
218
|
+
cmd.append(prompt)
|
|
219
|
+
return cmd
|
|
220
|
+
|
|
221
|
+
def parse_output(self, stdout: str, stderr: str) -> tuple[str, TimingData]:
|
|
222
|
+
timing = TimingData()
|
|
223
|
+
final_text = ""
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
data = json.loads(stdout)
|
|
227
|
+
if isinstance(data, dict):
|
|
228
|
+
final_text = data.get("result", "")
|
|
229
|
+
usage = data.get("usage", {})
|
|
230
|
+
timing.input_tokens = usage.get("input_tokens", 0)
|
|
231
|
+
timing.output_tokens = usage.get("output_tokens", 0)
|
|
232
|
+
timing.cached_tokens = usage.get("cache_read_input_tokens", 0)
|
|
233
|
+
timing.total_tokens = timing.input_tokens + timing.output_tokens
|
|
234
|
+
duration_s = data.get("duration_ms", 0)
|
|
235
|
+
if duration_s:
|
|
236
|
+
timing.duration_ms = duration_s
|
|
237
|
+
except json.JSONDecodeError:
|
|
238
|
+
final_text = stdout
|
|
239
|
+
|
|
240
|
+
return final_text, timing
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CodexHarness(AgentHarness):
|
|
244
|
+
agent_type = AgentType.CODEX
|
|
245
|
+
base_url_env = "OPENAI_BASE_URL"
|
|
246
|
+
|
|
247
|
+
def build_command(self, prompt: str, output_dir: Path) -> list[str]:
|
|
248
|
+
cmd = [
|
|
249
|
+
"codex",
|
|
250
|
+
"exec",
|
|
251
|
+
"--json",
|
|
252
|
+
"--full-auto",
|
|
253
|
+
]
|
|
254
|
+
if self.model:
|
|
255
|
+
cmd.extend(["--model", self.model])
|
|
256
|
+
cmd.append(prompt)
|
|
257
|
+
return cmd
|
|
258
|
+
|
|
259
|
+
def parse_output(self, stdout: str, stderr: str) -> tuple[str, TimingData]:
|
|
260
|
+
timing = TimingData()
|
|
261
|
+
messages = []
|
|
262
|
+
|
|
263
|
+
for line in stdout.strip().split("\n"):
|
|
264
|
+
if not line.strip():
|
|
265
|
+
continue
|
|
266
|
+
try:
|
|
267
|
+
event = json.loads(line)
|
|
268
|
+
etype = event.get("type", "")
|
|
269
|
+
|
|
270
|
+
if etype == "item.completed":
|
|
271
|
+
item = event.get("item", {})
|
|
272
|
+
if item.get("type") == "agent_message":
|
|
273
|
+
messages.append(item.get("text", ""))
|
|
274
|
+
|
|
275
|
+
elif etype == "turn.completed":
|
|
276
|
+
usage = event.get("usage", {})
|
|
277
|
+
timing.input_tokens += usage.get("input_tokens", 0)
|
|
278
|
+
timing.output_tokens += usage.get("output_tokens", 0)
|
|
279
|
+
timing.cached_tokens += usage.get("cached_input_tokens", 0)
|
|
280
|
+
|
|
281
|
+
except json.JSONDecodeError:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
timing.total_tokens = timing.input_tokens + timing.output_tokens
|
|
285
|
+
return "\n".join(messages), timing
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class FakeHarness(AgentHarness):
|
|
289
|
+
agent_type = AgentType.FAKE
|
|
290
|
+
|
|
291
|
+
def build_command(self, prompt: str, output_dir: Path) -> list[str]:
|
|
292
|
+
return ["fake-agent", prompt]
|
|
293
|
+
|
|
294
|
+
def parse_output(self, stdout: str, stderr: str) -> tuple[str, TimingData]:
|
|
295
|
+
timing = TimingData(total_tokens=1, input_tokens=1, duration_ms=1)
|
|
296
|
+
return stdout, timing
|
|
297
|
+
|
|
298
|
+
def run(self, prompt: str, output_dir: Path) -> tuple[str, TimingData, str, str]:
|
|
299
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
skill_installed = any((self.workspace / ".fake" / "skills").glob("*/SKILL.md"))
|
|
301
|
+
if skill_installed and ("format" in prompt.lower() or "$format-json" in prompt.lower()):
|
|
302
|
+
final_output = '{\n "formatted": true,\n "status": "formatted-json-ok"\n}'
|
|
303
|
+
else:
|
|
304
|
+
final_output = "fake-agent baseline output: no formatting performed"
|
|
305
|
+
|
|
306
|
+
stdout = final_output
|
|
307
|
+
stderr = ""
|
|
308
|
+
timing = TimingData(
|
|
309
|
+
total_tokens=1, input_tokens=1, output_tokens=0, cached_tokens=0, duration_ms=1, exit_code=0
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
with open(output_dir / "stdout.log", "w", encoding="utf-8") as f:
|
|
313
|
+
f.write(stdout)
|
|
314
|
+
with open(output_dir / "stderr.log", "w", encoding="utf-8") as f:
|
|
315
|
+
f.write(stderr)
|
|
316
|
+
|
|
317
|
+
return final_output, timing, stdout, stderr
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
HARNESSES: dict[AgentType, type[AgentHarness]] = {
|
|
321
|
+
AgentType.OPENCODE: OpenCodeHarness,
|
|
322
|
+
AgentType.CLAUDE_CODE: ClaudeCodeHarness,
|
|
323
|
+
AgentType.CODEX: CodexHarness,
|
|
324
|
+
AgentType.FAKE: FakeHarness,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_harness(
|
|
329
|
+
agent_type: AgentType,
|
|
330
|
+
workspace: Path,
|
|
331
|
+
model: Optional[str] = None,
|
|
332
|
+
base_url: Optional[str] = None,
|
|
333
|
+
timeout: Optional[int] = None,
|
|
334
|
+
max_retries: Optional[int] = None,
|
|
335
|
+
) -> AgentHarness:
|
|
336
|
+
cls = HARNESSES[agent_type]
|
|
337
|
+
return cls(workspace, model, base_url=base_url, timeout=timeout, max_retries=max_retries)
|
skill_eval/models.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentType(str, Enum):
|
|
11
|
+
OPENCODE = "opencode"
|
|
12
|
+
CLAUDE_CODE = "claude-code"
|
|
13
|
+
CODEX = "codex"
|
|
14
|
+
FAKE = "fake"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvalCase(BaseModel):
|
|
18
|
+
id: int | str
|
|
19
|
+
prompt: str
|
|
20
|
+
expected_output: str
|
|
21
|
+
files: list[str] = Field(default_factory=list)
|
|
22
|
+
assertions: list[str] = Field(default_factory=list)
|
|
23
|
+
should_trigger: bool = True
|
|
24
|
+
force_skill_invocation: bool = False
|
|
25
|
+
stage_files: bool = False
|
|
26
|
+
|
|
27
|
+
@model_validator(mode="after")
|
|
28
|
+
def validate_contradiction(self) -> EvalCase:
|
|
29
|
+
if self.force_skill_invocation and not self.should_trigger:
|
|
30
|
+
raise ValueError("Contradiction: force_skill_invocation=True cannot be set when should_trigger=False.")
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EvalSuite(BaseModel):
|
|
35
|
+
skill_name: str
|
|
36
|
+
evals: list[EvalCase]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GitStateSnapshot(BaseModel):
|
|
40
|
+
local_branches: list[str] = Field(default_factory=list)
|
|
41
|
+
remote_branches: list[str] = Field(default_factory=list)
|
|
42
|
+
current_branch: str = ""
|
|
43
|
+
head_sha: str = ""
|
|
44
|
+
commit_count: int = 0
|
|
45
|
+
commits: list[str] = Field(default_factory=list)
|
|
46
|
+
remote_names: list[str] = Field(default_factory=list)
|
|
47
|
+
open_prs: list[dict] = Field(default_factory=list)
|
|
48
|
+
branch_heads: dict[str, str] = Field(default_factory=dict)
|
|
49
|
+
remote_branch_heads: dict[str, str] = Field(default_factory=dict)
|
|
50
|
+
commit_shas: list[str] = Field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RunMeta(BaseModel):
|
|
54
|
+
eval_id: int | str
|
|
55
|
+
agent: str
|
|
56
|
+
with_skill: bool
|
|
57
|
+
iteration: int = 1
|
|
58
|
+
skill_name: str = ""
|
|
59
|
+
source_repo: Optional[str] = None
|
|
60
|
+
run_id: str = ""
|
|
61
|
+
run_index: int = 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CleanupManifest(BaseModel):
|
|
65
|
+
source_repo: Optional[str] = None
|
|
66
|
+
source_repo_slug: Optional[str] = None
|
|
67
|
+
remote_branches: list[str] = Field(default_factory=list)
|
|
68
|
+
pr_numbers: list[int] = Field(default_factory=list)
|
|
69
|
+
workspaces: list[str] = Field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TimingData(BaseModel):
|
|
73
|
+
total_tokens: int = 0
|
|
74
|
+
input_tokens: int = 0
|
|
75
|
+
output_tokens: int = 0
|
|
76
|
+
cached_tokens: int = 0
|
|
77
|
+
duration_ms: int = 0
|
|
78
|
+
exit_code: Optional[int] = None
|
|
79
|
+
timed_out: bool = False
|
|
80
|
+
retries: int = 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AssertionResult(BaseModel):
|
|
84
|
+
text: str
|
|
85
|
+
passed: bool
|
|
86
|
+
evidence: str
|
|
87
|
+
method: str = "deterministic"
|
|
88
|
+
skipped: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GradingResult(BaseModel):
|
|
92
|
+
assertion_results: list[AssertionResult]
|
|
93
|
+
summary: GradingSummary
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GradingSummary(BaseModel):
|
|
97
|
+
passed: int
|
|
98
|
+
failed: int
|
|
99
|
+
total: int
|
|
100
|
+
pass_rate: float
|
|
101
|
+
skipped: int = 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
GradingResult.model_rebuild()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RunConfig(BaseModel):
|
|
108
|
+
skill_path: Path
|
|
109
|
+
evals_path: Path
|
|
110
|
+
workspace_path: Path
|
|
111
|
+
agents: list[AgentType]
|
|
112
|
+
concurrency: int = 1
|
|
113
|
+
with_baseline: bool = True
|
|
114
|
+
grader_model: str = "deepseek/deepseek-v4-flash"
|
|
115
|
+
grader_base_url: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BenchmarkStats(BaseModel):
|
|
119
|
+
pass_rate: StatsPair
|
|
120
|
+
time_seconds: StatsPair
|
|
121
|
+
tokens: StatsPair
|
|
122
|
+
# Fraction of runs where ALL assertions passed (pass@1 estimate).
|
|
123
|
+
full_pass_rate: float = 0.0
|
|
124
|
+
# Fraction of evals where at least one of k runs fully passed.
|
|
125
|
+
pass_at_k: float = 0.0
|
|
126
|
+
k: int = 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class StatsPair(BaseModel):
|
|
130
|
+
mean: float
|
|
131
|
+
stddev: float
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class BenchmarkResult(BaseModel):
|
|
135
|
+
run_summary: dict[str, BenchmarkStats]
|
|
136
|
+
# Per-agent with_skill - without_skill deltas, keyed by agent value.
|
|
137
|
+
deltas: dict[str, DeltaStats] = Field(default_factory=dict)
|
|
138
|
+
# Kept for backward compatibility: equals the single agent's delta when
|
|
139
|
+
# exactly one agent was run, otherwise None.
|
|
140
|
+
delta: Optional[DeltaStats] = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class DeltaStats(BaseModel):
|
|
144
|
+
pass_rate: float
|
|
145
|
+
time_seconds: float
|
|
146
|
+
tokens: float
|