coder-eval 0.8.2__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.
- coder_eval/.gitattributes +2 -0
- coder_eval/__init__.py +3 -0
- coder_eval/agent.py +302 -0
- coder_eval/agents/__init__.py +41 -0
- coder_eval/agents/_logging.py +89 -0
- coder_eval/agents/antigravity_agent.py +811 -0
- coder_eval/agents/claude_code_agent.py +1701 -0
- coder_eval/agents/codex_agent.py +2055 -0
- coder_eval/agents/noop_agent.py +100 -0
- coder_eval/agents/registry.py +163 -0
- coder_eval/agents/watchdog.py +116 -0
- coder_eval/analysis.py +88 -0
- coder_eval/cli/__init__.py +87 -0
- coder_eval/cli/aggregate_command.py +153 -0
- coder_eval/cli/console.py +7 -0
- coder_eval/cli/evaluate_command.py +164 -0
- coder_eval/cli/plan_command.py +152 -0
- coder_eval/cli/report_command.py +121 -0
- coder_eval/cli/run_command.py +686 -0
- coder_eval/cli/run_helpers.py +137 -0
- coder_eval/cli/run_task_internal_command.py +210 -0
- coder_eval/cli/utils.py +37 -0
- coder_eval/config.py +212 -0
- coder_eval/criteria/__init__.py +163 -0
- coder_eval/criteria/_classification_aggregate.py +106 -0
- coder_eval/criteria/agent_judge.py +425 -0
- coder_eval/criteria/base.py +248 -0
- coder_eval/criteria/classification_match.py +107 -0
- coder_eval/criteria/command_executed.py +181 -0
- coder_eval/criteria/commands_efficiency.py +73 -0
- coder_eval/criteria/file_check.py +119 -0
- coder_eval/criteria/file_contains.py +85 -0
- coder_eval/criteria/file_exists.py +45 -0
- coder_eval/criteria/file_matches_regex.py +86 -0
- coder_eval/criteria/json_check.py +184 -0
- coder_eval/criteria/llm_judge.py +260 -0
- coder_eval/criteria/reference_comparison.py +130 -0
- coder_eval/criteria/run_command.py +220 -0
- coder_eval/criteria/skill_triggered.py +111 -0
- coder_eval/criteria/uipath_eval.py +223 -0
- coder_eval/errors/__init__.py +26 -0
- coder_eval/errors/agent.py +26 -0
- coder_eval/errors/budget.py +28 -0
- coder_eval/errors/categories.py +205 -0
- coder_eval/errors/categorization.py +240 -0
- coder_eval/errors/executor.py +124 -0
- coder_eval/errors/judge.py +12 -0
- coder_eval/errors/retry.py +211 -0
- coder_eval/errors/timeout.py +63 -0
- coder_eval/evaluation/__init__.py +10 -0
- coder_eval/evaluation/checker.py +260 -0
- coder_eval/evaluation/judge_anthropic.py +55 -0
- coder_eval/evaluation/judge_bedrock.py +114 -0
- coder_eval/evaluation/judge_context.py +497 -0
- coder_eval/evaluation/judge_models.py +53 -0
- coder_eval/evaluation/judge_persistence.py +291 -0
- coder_eval/evaluation/judge_usage.py +50 -0
- coder_eval/evaluation/sub_agent.py +231 -0
- coder_eval/evaluation/summaries.py +48 -0
- coder_eval/evaluation/verdict_tool.py +213 -0
- coder_eval/formatting.py +191 -0
- coder_eval/isolation/__init__.py +15 -0
- coder_eval/isolation/docker_runner.py +1253 -0
- coder_eval/logging_config.py +399 -0
- coder_eval/models/__init__.py +337 -0
- coder_eval/models/agent_config.py +373 -0
- coder_eval/models/container_paths.py +26 -0
- coder_eval/models/criteria.py +942 -0
- coder_eval/models/enums.py +121 -0
- coder_eval/models/experiment.py +314 -0
- coder_eval/models/judge.py +90 -0
- coder_eval/models/judge_defaults.py +11 -0
- coder_eval/models/limits.py +89 -0
- coder_eval/models/merge_strategy.py +132 -0
- coder_eval/models/mutations.py +95 -0
- coder_eval/models/results.py +787 -0
- coder_eval/models/routing.py +160 -0
- coder_eval/models/sandbox.py +371 -0
- coder_eval/models/tasks.py +631 -0
- coder_eval/models/telemetry.py +454 -0
- coder_eval/models/templates.py +108 -0
- coder_eval/orchestration/__init__.py +13 -0
- coder_eval/orchestration/batch.py +690 -0
- coder_eval/orchestration/config.py +111 -0
- coder_eval/orchestration/config_merge.py +431 -0
- coder_eval/orchestration/evaluation.py +94 -0
- coder_eval/orchestration/experiment.py +837 -0
- coder_eval/orchestration/overrides.py +206 -0
- coder_eval/orchestration/task_loader.py +437 -0
- coder_eval/orchestrator.py +2078 -0
- coder_eval/path_utils.py +79 -0
- coder_eval/plugins.py +81 -0
- coder_eval/pricing.py +169 -0
- coder_eval/py.typed +0 -0
- coder_eval/reports.py +1066 -0
- coder_eval/reports_experiment.py +761 -0
- coder_eval/reports_html.py +1659 -0
- coder_eval/reports_stats.py +250 -0
- coder_eval/resources/__init__.py +137 -0
- coder_eval/resources/default_experiment.yaml +85 -0
- coder_eval/resources/default_ignore_patterns.yaml +60 -0
- coder_eval/resources/tags.yaml +55 -0
- coder_eval/sandbox.py +1127 -0
- coder_eval/scoring/__init__.py +11 -0
- coder_eval/scoring/ast_similarity.py +38 -0
- coder_eval/scoring/complexity.py +115 -0
- coder_eval/scoring/quality.py +154 -0
- coder_eval/scoring/signature_similarity.py +57 -0
- coder_eval/scoring/similarity.py +103 -0
- coder_eval/scoring/token_similarity.py +44 -0
- coder_eval/simulation/__init__.py +27 -0
- coder_eval/simulation/termination.py +88 -0
- coder_eval/simulation/user_simulator.py +324 -0
- coder_eval/streaming/__init__.py +44 -0
- coder_eval/streaming/callbacks.py +57 -0
- coder_eval/streaming/collector.py +193 -0
- coder_eval/streaming/events.py +198 -0
- coder_eval/streaming/renderers.py +248 -0
- coder_eval/streaming/wire.py +115 -0
- coder_eval/telemetry.py +407 -0
- coder_eval/utils.py +517 -0
- coder_eval-0.8.2.dist-info/METADATA +242 -0
- coder_eval-0.8.2.dist-info/RECORD +127 -0
- coder_eval-0.8.2.dist-info/WHEEL +4 -0
- coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
- coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
- coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
"""Run command - execute evaluation tasks."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import typer
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
|
|
15
|
+
from ..config import settings
|
|
16
|
+
from ..logging_config import setup_logging
|
|
17
|
+
from ..models import PreservationMode, ResolvedTask, RunSummary, TaskResult
|
|
18
|
+
from ..orchestration.config import BatchRunConfig
|
|
19
|
+
from ..path_utils import create_latest_symlink, format_task_log_id
|
|
20
|
+
from ..streaming.callbacks import CompositeStreamCallback
|
|
21
|
+
from ..streaming.renderers import LoggingStreamRenderer, RichStreamRenderer
|
|
22
|
+
from .console import console
|
|
23
|
+
from .run_helpers import (
|
|
24
|
+
discover_default_tasks,
|
|
25
|
+
expand_task_files,
|
|
26
|
+
prepare_run_directory,
|
|
27
|
+
print_execution_mode,
|
|
28
|
+
print_execution_summary,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_experiment_path(experiment: Path | None) -> Path | None:
|
|
33
|
+
"""Resolve an experiment path, supporting bare names like 'model-comparison'.
|
|
34
|
+
|
|
35
|
+
Resolution order:
|
|
36
|
+
1. None → None (use default experiment)
|
|
37
|
+
2. Path exists as-is → use it
|
|
38
|
+
3. experiments/{name}.yaml exists → use it
|
|
39
|
+
4. experiments/{name} exists → use it
|
|
40
|
+
5. Raise typer.BadParameter with available experiments
|
|
41
|
+
"""
|
|
42
|
+
if experiment is None:
|
|
43
|
+
return None
|
|
44
|
+
if experiment.exists():
|
|
45
|
+
return experiment
|
|
46
|
+
|
|
47
|
+
# Try resolving bare name under experiments/ (project-root-relative, not CWD-relative).
|
|
48
|
+
# Path: cli/run_command.py → cli/ → coder_eval/ → src/ → project_root (4 levels).
|
|
49
|
+
# NOTE: This assumes a source checkout. If installed into site-packages, this won't resolve.
|
|
50
|
+
# That's acceptable since experiments/ lives in the repo, not the installed package.
|
|
51
|
+
_project_root = Path(__file__).resolve().parent.parent.parent.parent
|
|
52
|
+
experiments_dir = _project_root / "experiments"
|
|
53
|
+
for candidate in [
|
|
54
|
+
experiments_dir / f"{experiment}.yaml",
|
|
55
|
+
experiments_dir / f"{experiment}.yml",
|
|
56
|
+
experiments_dir / str(experiment),
|
|
57
|
+
]:
|
|
58
|
+
if candidate.exists():
|
|
59
|
+
return candidate
|
|
60
|
+
|
|
61
|
+
# Build helpful error message listing available experiments
|
|
62
|
+
available: list[str] = []
|
|
63
|
+
if experiments_dir.is_dir():
|
|
64
|
+
available = sorted(p.stem for p in experiments_dir.glob("*.yaml") if p.stem != "default")
|
|
65
|
+
hint = f" Available: {', '.join(available)}" if available else ""
|
|
66
|
+
raise typer.BadParameter(f"Experiment not found: {experiment}.{hint}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_overrides(
|
|
70
|
+
*,
|
|
71
|
+
model: str | None,
|
|
72
|
+
driver: str | None,
|
|
73
|
+
set_overrides: list[str],
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Translate the surviving alias flags (``--model`` / ``--driver``) +
|
|
76
|
+
``-D``/``--set`` entries into one validated override map (dotted path ->
|
|
77
|
+
typed value).
|
|
78
|
+
|
|
79
|
+
Alias flags and ``-D`` share the same engine path. A path set by both an
|
|
80
|
+
alias and ``-D`` (or by two ``-D`` entries) is a hard error so they never
|
|
81
|
+
silently last-win against each other. Every resulting path is validated
|
|
82
|
+
against the schema; ``OverrideError`` is wrapped into ``typer.BadParameter``
|
|
83
|
+
at this CLI boundary. All other task-config knobs (permission mode, turn
|
|
84
|
+
limits, timeouts, tools, plugins, SDK options) are expressed via ``-D``.
|
|
85
|
+
"""
|
|
86
|
+
from ..orchestration.config_merge import MergeError, validate_paths
|
|
87
|
+
from ..orchestration.overrides import OverrideError, parse_override
|
|
88
|
+
|
|
89
|
+
overrides: dict[str, Any] = {}
|
|
90
|
+
sources: dict[str, str] = {} # path -> originating flag, for collision messages
|
|
91
|
+
|
|
92
|
+
def _add_alias(path: str, value: Any, flag: str) -> None:
|
|
93
|
+
overrides[path] = value
|
|
94
|
+
sources[path] = flag
|
|
95
|
+
|
|
96
|
+
if model is not None:
|
|
97
|
+
_add_alias("agent.model", model, "--model")
|
|
98
|
+
if driver is not None:
|
|
99
|
+
_add_alias("sandbox.driver", driver, "--driver")
|
|
100
|
+
|
|
101
|
+
# -D / --set entries (hard error on collision with an alias or another -D).
|
|
102
|
+
for raw in set_overrides:
|
|
103
|
+
try:
|
|
104
|
+
path, value = parse_override(raw)
|
|
105
|
+
except OverrideError as e:
|
|
106
|
+
raise typer.BadParameter(str(e)) from e
|
|
107
|
+
if path in sources:
|
|
108
|
+
prior = sources[path]
|
|
109
|
+
if prior == "-D":
|
|
110
|
+
raise typer.BadParameter(f"{path!r} set by -D more than once; specify it once")
|
|
111
|
+
raise typer.BadParameter(f"{path!r} set by both {prior} and -D; specify it once")
|
|
112
|
+
overrides[path] = value
|
|
113
|
+
sources[path] = "-D"
|
|
114
|
+
|
|
115
|
+
# Validate every path against the resolved-TaskDefinition schema (the same
|
|
116
|
+
# walk the resolver uses). MergeError carries the did-you-mean suggestion.
|
|
117
|
+
try:
|
|
118
|
+
validate_paths(list(overrides))
|
|
119
|
+
except MergeError as e:
|
|
120
|
+
raise typer.BadParameter(str(e)) from e
|
|
121
|
+
|
|
122
|
+
return overrides
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_command(
|
|
126
|
+
task_files: list[Path] | None = typer.Argument( # noqa: B008
|
|
127
|
+
None,
|
|
128
|
+
help="Path(s) to task YAML file(s). Defaults to all tasks/ recursively.",
|
|
129
|
+
),
|
|
130
|
+
preservation_mode: PreservationMode | None = typer.Option( # noqa: B008
|
|
131
|
+
None,
|
|
132
|
+
"--preservation-mode",
|
|
133
|
+
help=(
|
|
134
|
+
"How to persist each task's sandbox: NONE (delete), MOVE_ON_WRITE "
|
|
135
|
+
"(run in a tempdir, move into run_dir/artifacts), or DIRECT_WRITE "
|
|
136
|
+
"(run directly in run_dir/artifacts). Default is driver-derived — "
|
|
137
|
+
"docker → DIRECT_WRITE, else MOVE_ON_WRITE. Explicit value always wins."
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
run_dir: Path | None = typer.Option( # noqa: B008
|
|
141
|
+
None,
|
|
142
|
+
"--run-dir",
|
|
143
|
+
help="Custom run directory (default: auto-generated timestamped directory in runs/)",
|
|
144
|
+
),
|
|
145
|
+
resume: bool = typer.Option(
|
|
146
|
+
False,
|
|
147
|
+
"--resume",
|
|
148
|
+
help=(
|
|
149
|
+
"Resume an interrupted run: skip tasks already finalized in --run-dir and "
|
|
150
|
+
"run only the rest, folding prior results into run.json. A task counts as "
|
|
151
|
+
"finalized once it has ANY final status — including FAILED/ERROR — so resume "
|
|
152
|
+
"does NOT retry failures (delete a task's task.json to force a re-run). "
|
|
153
|
+
"Requires --run-dir. A config mismatch (model/backend/flags) is warned, not "
|
|
154
|
+
"refused — the resumed tasks keep their original-config results, so the run "
|
|
155
|
+
"mixes configs; use a fresh --run-dir to keep configs separate."
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
max_parallel: int = typer.Option(
|
|
159
|
+
1,
|
|
160
|
+
"--max-parallel",
|
|
161
|
+
"-j",
|
|
162
|
+
help="Maximum number of tasks to run concurrently (default: 1 = sequential)",
|
|
163
|
+
min=1,
|
|
164
|
+
),
|
|
165
|
+
verbose: bool = typer.Option(
|
|
166
|
+
False,
|
|
167
|
+
"--verbose",
|
|
168
|
+
"-v",
|
|
169
|
+
help="Enable verbose (DEBUG level) logging",
|
|
170
|
+
),
|
|
171
|
+
log_file: Path | None = typer.Option( # noqa: B008
|
|
172
|
+
None,
|
|
173
|
+
"--log-file",
|
|
174
|
+
help="Log to file in addition to console",
|
|
175
|
+
),
|
|
176
|
+
tags: str | None = typer.Option(
|
|
177
|
+
None,
|
|
178
|
+
"--tags",
|
|
179
|
+
"-t",
|
|
180
|
+
help="Only run tasks matching any of these tags (comma-separated, e.g., 'smoke,golden')",
|
|
181
|
+
),
|
|
182
|
+
exclude_tags: str | None = typer.Option(
|
|
183
|
+
None,
|
|
184
|
+
"--exclude-tags",
|
|
185
|
+
help="Skip tasks matching any of these tags (comma-separated, e.g., 'example,integration')",
|
|
186
|
+
),
|
|
187
|
+
include_skipped: bool = typer.Option(
|
|
188
|
+
False,
|
|
189
|
+
"--include-skipped",
|
|
190
|
+
help=(
|
|
191
|
+
"Also run tasks marked `skip: true` in their YAML. Off by default so the "
|
|
192
|
+
"nightly/CI keep excluding them; use for on-demand / local runs of "
|
|
193
|
+
"quarantined or opt-in tasks."
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
agent_type: str | None = typer.Option(
|
|
197
|
+
None,
|
|
198
|
+
"--type",
|
|
199
|
+
"-T",
|
|
200
|
+
# Open string, not a closed click.Choice: the agent registry (incl. plugin
|
|
201
|
+
# kinds discovered at startup) is the source of truth, and it isn't populated
|
|
202
|
+
# at CLI-definition time. An unregistered kind fails at parse_agent_config with
|
|
203
|
+
# a clear "No agent registered for type ...; Registered kinds: [...]" message.
|
|
204
|
+
help="Override agent type for all tasks (e.g. 'claude-code', 'codex', or a plugin kind)",
|
|
205
|
+
),
|
|
206
|
+
model: str | None = typer.Option(
|
|
207
|
+
None,
|
|
208
|
+
"--model",
|
|
209
|
+
"-m",
|
|
210
|
+
help="Override agent model for all tasks (e.g., claude-sonnet-4-20250514)",
|
|
211
|
+
),
|
|
212
|
+
stream: str | None = typer.Option(
|
|
213
|
+
None,
|
|
214
|
+
"--stream",
|
|
215
|
+
"-s",
|
|
216
|
+
click_type=click.Choice(["full", "minimal"], case_sensitive=False),
|
|
217
|
+
help="Stream LLM events to terminal: 'full' or 'minimal' (turn-level only). Disables progress bar.",
|
|
218
|
+
),
|
|
219
|
+
backend: str | None = typer.Option(
|
|
220
|
+
None,
|
|
221
|
+
"--backend",
|
|
222
|
+
"-b",
|
|
223
|
+
click_type=click.Choice(["direct", "bedrock"], case_sensitive=False),
|
|
224
|
+
help="API backend (default: from API_BACKEND env var)",
|
|
225
|
+
),
|
|
226
|
+
experiment: Path | None = typer.Option( # noqa: B008
|
|
227
|
+
None,
|
|
228
|
+
"--experiment",
|
|
229
|
+
"-e",
|
|
230
|
+
help="Experiment definition YAML (default: experiments/default.yaml)",
|
|
231
|
+
),
|
|
232
|
+
sample: int | None = typer.Option(
|
|
233
|
+
None,
|
|
234
|
+
"--sample",
|
|
235
|
+
help=(
|
|
236
|
+
"For dataset-backed tasks, use a random N-row sample "
|
|
237
|
+
"(fixed seed: reproducible, unbiased across paths). Cheap dataset smoke-test."
|
|
238
|
+
),
|
|
239
|
+
min=1,
|
|
240
|
+
),
|
|
241
|
+
sample_per_stratum: int | None = typer.Option(
|
|
242
|
+
None,
|
|
243
|
+
"--sample-per-stratum",
|
|
244
|
+
help=(
|
|
245
|
+
"For dataset-backed tasks, keep up to N rows per stratum (stratify_field, "
|
|
246
|
+
"default expected_skill) — a stratified sample that overrides the task's "
|
|
247
|
+
"dataset.sample_per_stratum without editing the YAML. Ignored when --sample is set. "
|
|
248
|
+
"Nondeterministic (re-draws each run) unless the task sets dataset.sample_seed."
|
|
249
|
+
),
|
|
250
|
+
min=1,
|
|
251
|
+
),
|
|
252
|
+
repeats: int | None = typer.Option(
|
|
253
|
+
None,
|
|
254
|
+
"--repeats",
|
|
255
|
+
help="Run each (task, variant) N times. Overrides experiment/variant `repeats:`. Must be >=1.",
|
|
256
|
+
min=1,
|
|
257
|
+
),
|
|
258
|
+
# typer types this as str|None at signature level; click.Choice narrows
|
|
259
|
+
# the runtime value to {"tempdir","docker"}. BatchRunConfig.driver
|
|
260
|
+
# expects the Literal; the field validator accepts any str and the
|
|
261
|
+
# Choice constraint plus the experiment-layer Literal hint keep us safe.
|
|
262
|
+
driver: str | None = typer.Option(
|
|
263
|
+
None,
|
|
264
|
+
"--driver",
|
|
265
|
+
click_type=click.Choice(["tempdir", "docker"], case_sensitive=False),
|
|
266
|
+
help="Override sandbox driver for all tasks. 'docker' runs each task in a fresh container.",
|
|
267
|
+
),
|
|
268
|
+
set_overrides: list[str] = typer.Option( # noqa: B008
|
|
269
|
+
[],
|
|
270
|
+
"--set",
|
|
271
|
+
"-D",
|
|
272
|
+
metavar="PATH=VALUE",
|
|
273
|
+
help=(
|
|
274
|
+
"Override any resolved task-config field under agent/run_limits/sandbox, "
|
|
275
|
+
"e.g. -D run_limits.max_turns=30 -D agent.permission_mode=plan "
|
|
276
|
+
"-D agent.sdk_options.effort=high -D sandbox.docker.network=none. "
|
|
277
|
+
"Repeatable. Validated against the schema. A path set by both an alias "
|
|
278
|
+
"and -D is an error; values are YAML-parsed (on/off/yes/no stay strings). "
|
|
279
|
+
"(--model and --driver are shorthand aliases for -D agent.model / "
|
|
280
|
+
"-D sandbox.driver.)"
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Run evaluation tasks (optionally in parallel).
|
|
285
|
+
|
|
286
|
+
When no TASK_FILES are provided, all .yaml files under tasks/ are discovered recursively.
|
|
287
|
+
|
|
288
|
+
Sandboxes are preserved by default for debugging (driver-derived mode).
|
|
289
|
+
Use --preservation-mode NONE to clean up.
|
|
290
|
+
|
|
291
|
+
Examples:
|
|
292
|
+
|
|
293
|
+
coder-eval run
|
|
294
|
+
|
|
295
|
+
coder-eval run tasks/hello_date.yaml
|
|
296
|
+
|
|
297
|
+
coder-eval run tasks/*.yaml --preservation-mode NONE
|
|
298
|
+
|
|
299
|
+
coder-eval run tasks/*.yaml --run-dir ./my-custom-run
|
|
300
|
+
|
|
301
|
+
coder-eval run tasks/*.yaml --max-parallel 3
|
|
302
|
+
|
|
303
|
+
coder-eval run tasks/*.yaml --verbose --log-file debug.log
|
|
304
|
+
|
|
305
|
+
coder-eval run tasks/*.yaml --tags smoke
|
|
306
|
+
|
|
307
|
+
coder-eval run tasks/*.yaml --tags golden,basic --exclude-tags example
|
|
308
|
+
"""
|
|
309
|
+
# --resume needs an explicit run dir to resume into (auto-generated dirs are always fresh).
|
|
310
|
+
if resume and run_dir is None:
|
|
311
|
+
raise typer.BadParameter("--resume requires --run-dir pointing at the run to continue.")
|
|
312
|
+
|
|
313
|
+
# Parse tag filters
|
|
314
|
+
include_tags = {t.strip() for t in tags.split(",") if t.strip()} if tags else None
|
|
315
|
+
exclude_tags_set = {t.strip() for t in exclude_tags.split(",") if t.strip()} if exclude_tags else None
|
|
316
|
+
|
|
317
|
+
# Translate the surviving alias flags + -D/--set into one validated override
|
|
318
|
+
# map (layer 5). All other task-config knobs are expressed via -D.
|
|
319
|
+
overrides = _build_overrides(
|
|
320
|
+
model=model,
|
|
321
|
+
driver=driver,
|
|
322
|
+
set_overrides=set_overrides,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Override API backend if --backend was passed. The flag is shorthand for the
|
|
326
|
+
# API_BACKEND env var, so mirror it into os.environ as well: the docker driver
|
|
327
|
+
# forwards the backend into the container via the standard env passthrough
|
|
328
|
+
# (name-only `--env API_BACKEND`, which reads os.environ). A flag that only
|
|
329
|
+
# mutated `settings` would be dropped at the container boundary and the
|
|
330
|
+
# in-container Settings would silently default to DIRECT.
|
|
331
|
+
if backend is not None:
|
|
332
|
+
from coder_eval.models import ApiBackend
|
|
333
|
+
|
|
334
|
+
resolved_backend = ApiBackend(backend)
|
|
335
|
+
settings.api_backend = resolved_backend
|
|
336
|
+
os.environ["API_BACKEND"] = resolved_backend.value
|
|
337
|
+
|
|
338
|
+
# Setup logging before running tasks
|
|
339
|
+
log_level = settings.log_level
|
|
340
|
+
setup_logging(level=log_level, log_file=log_file, verbose=verbose)
|
|
341
|
+
|
|
342
|
+
# Default to discovering all tasks under tasks/ when none provided
|
|
343
|
+
resolved_task_files = task_files if task_files else discover_default_tasks()
|
|
344
|
+
|
|
345
|
+
# Resolve experiment path: bare names like "model-comparison" → experiments/model-comparison.yaml
|
|
346
|
+
resolved_experiment = _resolve_experiment_path(experiment)
|
|
347
|
+
|
|
348
|
+
# Run the async entry point
|
|
349
|
+
try:
|
|
350
|
+
asyncio.run(
|
|
351
|
+
_run_all_tasks(
|
|
352
|
+
resolved_task_files,
|
|
353
|
+
preservation_mode,
|
|
354
|
+
run_dir,
|
|
355
|
+
max_parallel,
|
|
356
|
+
include_tags,
|
|
357
|
+
exclude_tags_set,
|
|
358
|
+
agent_type,
|
|
359
|
+
overrides,
|
|
360
|
+
stream,
|
|
361
|
+
experiment_path=resolved_experiment,
|
|
362
|
+
max_rows=sample,
|
|
363
|
+
sample_per_stratum=sample_per_stratum,
|
|
364
|
+
repeats=repeats,
|
|
365
|
+
verbose=verbose,
|
|
366
|
+
resume=resume,
|
|
367
|
+
include_skipped=include_skipped,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
except KeyboardInterrupt:
|
|
371
|
+
console.print("\n[yellow]Execution interrupted.[/yellow]")
|
|
372
|
+
raise typer.Exit(2) from None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _run_all_tasks(
|
|
376
|
+
task_files: list[Path],
|
|
377
|
+
preservation_mode: PreservationMode | None,
|
|
378
|
+
run_dir: Path | None,
|
|
379
|
+
max_parallel: int,
|
|
380
|
+
include_tags: set[str] | None = None,
|
|
381
|
+
exclude_tags: set[str] | None = None,
|
|
382
|
+
agent_type: str | None = None,
|
|
383
|
+
overrides: dict[str, Any] | None = None,
|
|
384
|
+
stream_mode: str | None = None,
|
|
385
|
+
experiment_path: Path | None = None,
|
|
386
|
+
max_rows: int | None = None,
|
|
387
|
+
sample_per_stratum: int | None = None,
|
|
388
|
+
repeats: int | None = None,
|
|
389
|
+
verbose: bool = False,
|
|
390
|
+
resume: bool = False,
|
|
391
|
+
include_skipped: bool = False,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Async entry point for running all tasks (optionally in parallel).
|
|
394
|
+
|
|
395
|
+
Tasks are resolved through the experiment layer (defaulting to
|
|
396
|
+
experiments/default.yaml) and executed via run_batch.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
task_files: List of task file paths or glob patterns
|
|
400
|
+
preservation_mode: Sandbox preservation mode, or None for the driver-derived default
|
|
401
|
+
run_dir: Custom run directory (or None for auto-generated)
|
|
402
|
+
max_parallel: Maximum number of concurrent tasks
|
|
403
|
+
include_tags: Only run tasks matching any of these tags
|
|
404
|
+
exclude_tags: Skip tasks matching any of these tags
|
|
405
|
+
agent_type: Optional override for agent type (re-parses the union)
|
|
406
|
+
overrides: Generic layer-5 task-config overrides (path -> typed value)
|
|
407
|
+
from -D/--set and the bespoke flag aliases
|
|
408
|
+
stream_mode: Optional stream mode ('full' or 'minimal') for real-time output
|
|
409
|
+
experiment_path: Optional path to experiment YAML (default: experiments/default.yaml)
|
|
410
|
+
"""
|
|
411
|
+
# Prepare run directory
|
|
412
|
+
run_dir = prepare_run_directory(run_dir)
|
|
413
|
+
|
|
414
|
+
# Create 'latest' symlink immediately so it's available during the run
|
|
415
|
+
if run_dir.parent == settings.runs_dir:
|
|
416
|
+
create_latest_symlink(settings.runs_dir, run_dir.name)
|
|
417
|
+
|
|
418
|
+
# Expand glob patterns and collect task files
|
|
419
|
+
all_task_files = expand_task_files(task_files)
|
|
420
|
+
|
|
421
|
+
# Configure batch execution
|
|
422
|
+
config = BatchRunConfig(
|
|
423
|
+
run_dir=run_dir,
|
|
424
|
+
max_parallel=max_parallel,
|
|
425
|
+
preservation_mode=preservation_mode,
|
|
426
|
+
include_tags=include_tags,
|
|
427
|
+
exclude_tags=exclude_tags,
|
|
428
|
+
agent_type=agent_type,
|
|
429
|
+
overrides=overrides or {},
|
|
430
|
+
max_rows=max_rows,
|
|
431
|
+
sample_per_stratum=sample_per_stratum,
|
|
432
|
+
repeats=repeats,
|
|
433
|
+
verbose=verbose,
|
|
434
|
+
include_skipped=include_skipped,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
from ..telemetry import flush_telemetry, track_event
|
|
438
|
+
|
|
439
|
+
# TaskFileCount is the pre-expansion file count (dataset fan-out and variant
|
|
440
|
+
# resolution happen later); per-task counts are reconstructable from the
|
|
441
|
+
# CoderEval.Task.End events.
|
|
442
|
+
track_event(
|
|
443
|
+
"CoderEval.Run.Start",
|
|
444
|
+
{
|
|
445
|
+
"TaskFileCount": len(all_task_files),
|
|
446
|
+
"MaxParallel": max_parallel,
|
|
447
|
+
"AgentType": agent_type or "default",
|
|
448
|
+
"StreamMode": stream_mode or "none",
|
|
449
|
+
"Resume": resume,
|
|
450
|
+
"ExperimentProvided": experiment_path is not None,
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
# Always run through experiment layer (defaults to experiments/default.yaml)
|
|
456
|
+
summary, failed_suite_gates = await _run_with_experiment(
|
|
457
|
+
all_task_files, config, experiment_path, stream_mode, max_parallel, resume=resume
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Aggregate task logs into run.log
|
|
461
|
+
from ..logging_config import aggregate_task_logs
|
|
462
|
+
|
|
463
|
+
aggregate_task_logs(run_dir)
|
|
464
|
+
|
|
465
|
+
# Print execution summary
|
|
466
|
+
print_execution_summary(run_dir, summary)
|
|
467
|
+
finally:
|
|
468
|
+
# Explicit flush before process exit (belt-and-suspenders with atexit).
|
|
469
|
+
# In a `finally` so it runs on the success path and on any raised
|
|
470
|
+
# exception, but never catches/swallows the typer.Exit decided below.
|
|
471
|
+
flush_telemetry()
|
|
472
|
+
|
|
473
|
+
# Exit with non-zero code if any tasks failed, errored, or any suite failed its thresholds.
|
|
474
|
+
if summary.tasks_failed > 0 or summary.tasks_error > 0 or failed_suite_gates > 0:
|
|
475
|
+
raise typer.Exit(1)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
async def _run_with_callbacks(
|
|
479
|
+
execute_fn: Callable[..., Any],
|
|
480
|
+
task_count: int,
|
|
481
|
+
stream_mode: str | None,
|
|
482
|
+
) -> Any:
|
|
483
|
+
"""Run a batch execution function with streaming or progress bar callbacks.
|
|
484
|
+
|
|
485
|
+
Handles the shared logic of setting up either a streaming callback factory
|
|
486
|
+
(when --stream is enabled) or a tqdm progress bar (default mode).
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
execute_fn: Async callable that accepts keyword arguments
|
|
490
|
+
stream_callback_factory, on_task_complete, and on_batch_start.
|
|
491
|
+
task_count: Number of tasks (used for batch_mode detection).
|
|
492
|
+
stream_mode: Optional stream mode ('full' or 'minimal') for real-time output.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Whatever execute_fn returns.
|
|
496
|
+
"""
|
|
497
|
+
if stream_mode:
|
|
498
|
+
batch_mode = task_count > 1
|
|
499
|
+
rich_renderer = RichStreamRenderer(verbosity=stream_mode, batch_mode=batch_mode)
|
|
500
|
+
logging_renderer = LoggingStreamRenderer()
|
|
501
|
+
stream_callback_factory = lambda _task_id: CompositeStreamCallback([rich_renderer, logging_renderer]) # noqa: E731
|
|
502
|
+
return await execute_fn(stream_callback_factory=stream_callback_factory)
|
|
503
|
+
|
|
504
|
+
progress_bar: tqdm[Any] | None = None
|
|
505
|
+
|
|
506
|
+
def _on_batch_start(count: int) -> None:
|
|
507
|
+
nonlocal progress_bar
|
|
508
|
+
progress_bar = tqdm(total=count, desc="Tasks", unit="task", dynamic_ncols=True, disable=not sys.stderr.isatty())
|
|
509
|
+
|
|
510
|
+
def _on_task_complete(result: Any) -> None:
|
|
511
|
+
if progress_bar is None:
|
|
512
|
+
return
|
|
513
|
+
status = result.result.final_status
|
|
514
|
+
label = format_task_log_id(result.variant_id, result.task_id, result.replicate_index)
|
|
515
|
+
status_icon = status.icon
|
|
516
|
+
progress_bar.set_postfix_str(f"{status_icon} {label}")
|
|
517
|
+
progress_bar.update(1)
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
result = await execute_fn(on_task_complete=_on_task_complete, on_batch_start=_on_batch_start)
|
|
521
|
+
finally:
|
|
522
|
+
if progress_bar is not None:
|
|
523
|
+
progress_bar.close()
|
|
524
|
+
return result
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
async def _run_with_experiment(
|
|
528
|
+
all_task_files: list[Path],
|
|
529
|
+
config: BatchRunConfig,
|
|
530
|
+
experiment_path: Path | None,
|
|
531
|
+
stream_mode: str | None,
|
|
532
|
+
max_parallel: int,
|
|
533
|
+
resume: bool = False,
|
|
534
|
+
) -> tuple[RunSummary, int]:
|
|
535
|
+
"""Run tasks through the experiment resolution layer.
|
|
536
|
+
|
|
537
|
+
Loads experiments, resolves task configs (all 5 layers), executes via
|
|
538
|
+
run_batch, and generates experiment reports.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
all_task_files: Expanded list of task file paths.
|
|
542
|
+
config: Batch execution configuration.
|
|
543
|
+
experiment_path: Explicit experiment path or None for default.
|
|
544
|
+
stream_mode: Optional stream mode for real-time output.
|
|
545
|
+
max_parallel: Maximum parallel tasks (for batch_mode detection).
|
|
546
|
+
resume: Skip tasks already finalized in the run dir, folding their prior
|
|
547
|
+
results back into the summary.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
RunSummary with aggregated results.
|
|
551
|
+
"""
|
|
552
|
+
from ..orchestration.batch import (
|
|
553
|
+
clear_rerun_artifacts,
|
|
554
|
+
compute_run_fingerprint,
|
|
555
|
+
fingerprint_diff,
|
|
556
|
+
partition_for_resume,
|
|
557
|
+
read_run_fingerprint,
|
|
558
|
+
run_batch,
|
|
559
|
+
write_run_fingerprint,
|
|
560
|
+
)
|
|
561
|
+
from ..orchestration.experiment import (
|
|
562
|
+
DEFAULT_EXPERIMENT_PATH,
|
|
563
|
+
aggregate_results,
|
|
564
|
+
load_experiment,
|
|
565
|
+
resolve_all_tasks,
|
|
566
|
+
) # resolve_task_for_variant not needed here
|
|
567
|
+
from ..reports_experiment import ExperimentReportGenerator
|
|
568
|
+
|
|
569
|
+
# Load experiments (avoid double-loading when using default)
|
|
570
|
+
exp_path = experiment_path or DEFAULT_EXPERIMENT_PATH
|
|
571
|
+
try:
|
|
572
|
+
experiment = load_experiment(exp_path)
|
|
573
|
+
except (FileNotFoundError, ValueError) as e:
|
|
574
|
+
raise typer.BadParameter(f"Failed to load experiment '{exp_path}': {e}") from e
|
|
575
|
+
if exp_path == DEFAULT_EXPERIMENT_PATH:
|
|
576
|
+
default_experiment = experiment
|
|
577
|
+
elif DEFAULT_EXPERIMENT_PATH.exists():
|
|
578
|
+
try:
|
|
579
|
+
default_experiment = load_experiment(DEFAULT_EXPERIMENT_PATH)
|
|
580
|
+
except (FileNotFoundError, ValueError) as e:
|
|
581
|
+
raise typer.BadParameter(f"Failed to load default experiment '{DEFAULT_EXPERIMENT_PATH}': {e}") from e
|
|
582
|
+
else:
|
|
583
|
+
default_experiment = experiment # fall back to custom as its own baseline
|
|
584
|
+
|
|
585
|
+
# Resolve tasks through experiment layer (applies all 5 config layers).
|
|
586
|
+
# Layer-5 override failures (invalid -D value/path, sdk_options on a
|
|
587
|
+
# non-claude agent, the agent.type guard) and duplicate-task-id checks raise
|
|
588
|
+
# ValueError here; surface them as a clean CLI error instead of a traceback.
|
|
589
|
+
try:
|
|
590
|
+
resolved, skipped = resolve_all_tasks(
|
|
591
|
+
task_files=all_task_files,
|
|
592
|
+
experiment=experiment,
|
|
593
|
+
default_experiment=default_experiment,
|
|
594
|
+
config=config,
|
|
595
|
+
experiment_file=exp_path,
|
|
596
|
+
)
|
|
597
|
+
except ValueError as e:
|
|
598
|
+
raise typer.BadParameter(str(e)) from e
|
|
599
|
+
|
|
600
|
+
if skipped:
|
|
601
|
+
console.print(
|
|
602
|
+
f"[yellow]⚠[/] {len(skipped)} task file(s) skipped "
|
|
603
|
+
+ "(load errors or `skip: true` — see run.json `skipped_tasks` for reasons)"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Warn (don't refuse) when a --resume config differs from the original run. The
|
|
607
|
+
# per-task path key (variant/task_id/NN) doesn't encode the run config, so resumed
|
|
608
|
+
# tasks keep their original-config results — surfacing the mismatch makes the
|
|
609
|
+
# resulting mixed-config run.json visible instead of silent. Best-effort and
|
|
610
|
+
# informational: a missing stamp (run predates this feature) is tolerated.
|
|
611
|
+
current_fingerprint = compute_run_fingerprint(
|
|
612
|
+
config, experiment.experiment_id, settings.api_backend.value, settings.bedrock_model
|
|
613
|
+
)
|
|
614
|
+
if resume:
|
|
615
|
+
prior_fingerprint = read_run_fingerprint(config.run_dir)
|
|
616
|
+
if prior_fingerprint is not None:
|
|
617
|
+
diffs = fingerprint_diff(prior_fingerprint, current_fingerprint)
|
|
618
|
+
if diffs:
|
|
619
|
+
detail = "; ".join(f"{k}: {old!r} → {new!r}" for k, (old, new) in sorted(diffs.items()))
|
|
620
|
+
console.print(
|
|
621
|
+
f"[yellow]⚠[/] --resume into {config.run_dir} but the run config changed ({detail}). "
|
|
622
|
+
+ "Already-finalized tasks keep their original-config results, so this run mixes "
|
|
623
|
+
+ "configs — use a fresh --run-dir to keep them separate."
|
|
624
|
+
)
|
|
625
|
+
write_run_fingerprint(config.run_dir, current_fingerprint)
|
|
626
|
+
|
|
627
|
+
# On --resume, peel off tasks already finalized in the run dir. They are not
|
|
628
|
+
# re-executed but are folded back into run.json (and all downstream reports)
|
|
629
|
+
# via prior_results so the summary covers the whole run. `resolved` stays the
|
|
630
|
+
# full set — suite rollups below need every task, run or not.
|
|
631
|
+
to_run: list[ResolvedTask] = resolved
|
|
632
|
+
prior_results: list[TaskResult] = []
|
|
633
|
+
prior_resolved: list[ResolvedTask] = []
|
|
634
|
+
if resume:
|
|
635
|
+
to_run, prior_results, prior_resolved = partition_for_resume(resolved)
|
|
636
|
+
# A re-run task re-executes from scratch, so any leftover artifacts (only
|
|
637
|
+
# DIRECT_WRITE writes them live; a container killed mid-run leaves partials)
|
|
638
|
+
# are stale and could let a file-based criterion pass on the old output.
|
|
639
|
+
cleared = clear_rerun_artifacts(to_run)
|
|
640
|
+
console.print(
|
|
641
|
+
f"[cyan]↻ Resume:[/] {len(prior_results)} task(s) already complete, "
|
|
642
|
+
+ f"running {len(to_run)} remaining"
|
|
643
|
+
+ (f" (cleared {cleared} stale artifact dir(s))" if cleared else "")
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Print execution mode
|
|
647
|
+
print_execution_mode(len(to_run), max_parallel)
|
|
648
|
+
|
|
649
|
+
summary, task_results = await _run_with_callbacks(
|
|
650
|
+
execute_fn=lambda **kwargs: run_batch(
|
|
651
|
+
resolved_tasks=to_run,
|
|
652
|
+
config=config,
|
|
653
|
+
skipped_tasks=skipped,
|
|
654
|
+
prior_results=prior_results,
|
|
655
|
+
prior_resolved=prior_resolved,
|
|
656
|
+
**kwargs,
|
|
657
|
+
),
|
|
658
|
+
task_count=len(to_run),
|
|
659
|
+
stream_mode=stream_mode,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# Generate experiment reports
|
|
663
|
+
experiment_result = aggregate_results(
|
|
664
|
+
experiment_id=experiment.experiment_id,
|
|
665
|
+
description=experiment.description,
|
|
666
|
+
variant_ids=[v.variant_id for v in experiment.variants],
|
|
667
|
+
task_results=task_results,
|
|
668
|
+
total_duration=summary.total_duration_seconds,
|
|
669
|
+
)
|
|
670
|
+
# Reports are written at run root level (no experiment_id subfolder)
|
|
671
|
+
ExperimentReportGenerator.write_reports(experiment_result, config.run_dir, experiment=experiment)
|
|
672
|
+
|
|
673
|
+
# Per-suite pass-rate rollups for dataset-backed tasks (no-op when none were used).
|
|
674
|
+
# Pass `resolved` through so suite_thresholds on each criterion can be evaluated.
|
|
675
|
+
from ..reports import write_suite_rollups
|
|
676
|
+
|
|
677
|
+
rollups = write_suite_rollups(config.run_dir, task_results, resolved_tasks=resolved)
|
|
678
|
+
failed_gates = [r for r in rollups if not r.passed]
|
|
679
|
+
if failed_gates:
|
|
680
|
+
logging.getLogger(__name__).warning(
|
|
681
|
+
"%d suite gate(s) failed thresholds: %s",
|
|
682
|
+
len(failed_gates),
|
|
683
|
+
", ".join(f"{r.variant_id}/{r.suite_id}" for r in failed_gates),
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return summary, len(failed_gates)
|