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,111 @@
|
|
|
1
|
+
"""Skill-triggered criterion checker: did the agent engage the target skill?
|
|
2
|
+
|
|
3
|
+
Agent-agnostic. Claude Code engages a skill via an explicit ``Skill`` tool call;
|
|
4
|
+
Codex has no such tool — it auto-discovers skills under ``.agents/skills/`` and
|
|
5
|
+
engages one by reading its ``SKILL.md`` / references off disk via shell. Both
|
|
6
|
+
signals are detected here so the criterion scores identically across agents.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from coder_eval.criteria._classification_aggregate import overlay_classification_metrics
|
|
15
|
+
from coder_eval.criteria.base import BaseCriterion, register_criterion
|
|
16
|
+
from coder_eval.models import (
|
|
17
|
+
ClassificationCriterionResult,
|
|
18
|
+
CriterionAggregate,
|
|
19
|
+
CriterionResult,
|
|
20
|
+
SkillTriggeredCriterion,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from coder_eval.criteria.base import CheckContext
|
|
26
|
+
from coder_eval.models.results import TurnRecord
|
|
27
|
+
from coder_eval.models.telemetry import CommandTelemetry
|
|
28
|
+
from coder_eval.sandbox import Sandbox
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_YES = "yes"
|
|
33
|
+
_NO = "no"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _engaged_skill(cmd: CommandTelemetry, skill_name: str) -> bool:
|
|
37
|
+
"""True when one command engaged ``skill_name`` — agent-agnostically.
|
|
38
|
+
|
|
39
|
+
Claude: an explicit ``Skill`` tool call carries the skill in
|
|
40
|
+
``parameters['skill']`` (optionally namespaced, e.g. ``plugin:uipath-agents``).
|
|
41
|
+
|
|
42
|
+
Codex (and any non-Claude agent): no ``Skill`` tool exists, so the skill is
|
|
43
|
+
engaged by reading its files off disk via shell. Both the repo layout
|
|
44
|
+
(``.../skills/<skill_name>/...``) and the sandbox symlink
|
|
45
|
+
(``.agents/skills/<skill_name>/...``) contain the substring
|
|
46
|
+
``skills/<skill_name>/``, which appears in the recorded command string
|
|
47
|
+
(Bash ``parameters['command']``) or a file-path parameter. The trailing
|
|
48
|
+
slash prevents prefix collisions (``uipath-agents`` vs ``uipath-agents-foo``).
|
|
49
|
+
"""
|
|
50
|
+
if cmd.tool_name == "Skill" and cmd.parameters.get("skill", "").split(":")[-1] == skill_name:
|
|
51
|
+
return True
|
|
52
|
+
needle = f"skills/{skill_name}/"
|
|
53
|
+
return any(isinstance(v, str) and needle in v for v in cmd.parameters.values())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@register_criterion
|
|
57
|
+
class SkillTriggeredChecker(BaseCriterion[SkillTriggeredCriterion]):
|
|
58
|
+
"""Binary classifier: observed='yes' when the agent engaged the target skill.
|
|
59
|
+
|
|
60
|
+
Returns a ``ClassificationCriterionResult`` so the suite aggregator can
|
|
61
|
+
compute accuracy / recall / F1 / confusion matrix across all rows.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
criterion_type = "skill_triggered"
|
|
65
|
+
|
|
66
|
+
def _check_impl(
|
|
67
|
+
self,
|
|
68
|
+
criterion: SkillTriggeredCriterion,
|
|
69
|
+
sandbox: Sandbox,
|
|
70
|
+
reference_code: str | None = None,
|
|
71
|
+
*,
|
|
72
|
+
turn_records: list[TurnRecord] | None = None,
|
|
73
|
+
context: CheckContext | None = None,
|
|
74
|
+
) -> CriterionResult:
|
|
75
|
+
if turn_records is None:
|
|
76
|
+
return CriterionResult(
|
|
77
|
+
criterion_type=criterion.type,
|
|
78
|
+
description=criterion.description,
|
|
79
|
+
score=0.0,
|
|
80
|
+
details="No turn records available",
|
|
81
|
+
error="turn_records not provided to checker",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
triggered: bool = any(
|
|
85
|
+
_engaged_skill(cmd, criterion.skill_name) for turn in turn_records for cmd in turn.commands
|
|
86
|
+
)
|
|
87
|
+
expected_yes: bool = criterion.expected_skill == criterion.skill_name
|
|
88
|
+
score = 1.0 if triggered == expected_yes else 0.0
|
|
89
|
+
observed = _YES if triggered else _NO
|
|
90
|
+
expected = _YES if expected_yes else _NO
|
|
91
|
+
|
|
92
|
+
filt = f" (skill_name={criterion.skill_name!r})"
|
|
93
|
+
return ClassificationCriterionResult(
|
|
94
|
+
criterion_type=criterion.type,
|
|
95
|
+
description=criterion.description,
|
|
96
|
+
score=score,
|
|
97
|
+
details=f"observed={observed!r}, expected={expected!r}{filt}",
|
|
98
|
+
observed_label=observed,
|
|
99
|
+
expected_label=expected,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def aggregate(
|
|
103
|
+
self,
|
|
104
|
+
criterion: SkillTriggeredCriterion,
|
|
105
|
+
per_row_results: list[CriterionResult],
|
|
106
|
+
) -> CriterionAggregate | None:
|
|
107
|
+
"""Baseline stats (from super) + classification overlay (accuracy/F1/...)."""
|
|
108
|
+
base = super().aggregate(criterion, per_row_results)
|
|
109
|
+
if base is None:
|
|
110
|
+
return None
|
|
111
|
+
return overlay_classification_metrics(base, per_row_results)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""UiPath eval criterion checker."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
11
|
+
from coder_eval.models import CriterionResult, UiPathEvalCriterion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Detector for "the `uipath` CLI is not available in the sandbox". Sandboxes run
|
|
15
|
+
# on Linux (/bin/sh — dash on Ubuntu), so exit code 127 + "command not found" is
|
|
16
|
+
# the only shell signal we need to recognize. The regex also matches the python
|
|
17
|
+
# ModuleNotFoundError format for tasks that run `python -m uipath`.
|
|
18
|
+
_UIPATH_MISSING_PATTERN = re.compile(
|
|
19
|
+
r"\buipath\b.*command not found|no module named ['\"]?uipath['\"]?(?:\s|$)",
|
|
20
|
+
re.IGNORECASE,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from coder_eval.models.results import TurnRecord
|
|
26
|
+
from coder_eval.sandbox import Sandbox
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_criterion
|
|
32
|
+
class UiPathEvalChecker(BaseCriterion[UiPathEvalCriterion]):
|
|
33
|
+
"""Checker for UiPathEvalCriterion."""
|
|
34
|
+
|
|
35
|
+
criterion_type = "uipath_eval"
|
|
36
|
+
|
|
37
|
+
def _check_impl(
|
|
38
|
+
self,
|
|
39
|
+
criterion: UiPathEvalCriterion,
|
|
40
|
+
sandbox: "Sandbox",
|
|
41
|
+
reference_code: str | None = None,
|
|
42
|
+
*,
|
|
43
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
44
|
+
context: CheckContext | None = None,
|
|
45
|
+
) -> CriterionResult:
|
|
46
|
+
"""Check UiPath agent evaluation results against specified thresholds.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
criterion: UiPathEvalCriterion with agent_name, eval_set, and thresholds
|
|
50
|
+
sandbox: Sandbox instance for file access and command execution
|
|
51
|
+
reference_code: Not used for this criterion
|
|
52
|
+
turn_records: Not used for this criterion
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
CriterionResult with score (0.0-1.0) based on threshold compliance
|
|
56
|
+
"""
|
|
57
|
+
# Resolve eval_set path relative to task directory
|
|
58
|
+
eval_set_path = criterion.eval_set
|
|
59
|
+
task_dir = sandbox.task_dir
|
|
60
|
+
if task_dir:
|
|
61
|
+
resolved_path = (task_dir / criterion.eval_set).resolve()
|
|
62
|
+
if resolved_path.exists():
|
|
63
|
+
eval_set_path = str(resolved_path)
|
|
64
|
+
|
|
65
|
+
# Use a unique output filename to avoid conflicts
|
|
66
|
+
output_filename = f"uipath_eval_output_{uuid.uuid4().hex[:8]}.json"
|
|
67
|
+
|
|
68
|
+
# Run the evaluation command with properly escaped arguments
|
|
69
|
+
cmd = (
|
|
70
|
+
f"uv run uipath eval {shlex.quote(criterion.agent_name)} "
|
|
71
|
+
f"{shlex.quote(eval_set_path)} --no-report --output-file {shlex.quote(output_filename)}"
|
|
72
|
+
)
|
|
73
|
+
exit_code, _, stderr = sandbox.run_command(cmd)
|
|
74
|
+
|
|
75
|
+
if exit_code != 0:
|
|
76
|
+
stderr_text = stderr or ""
|
|
77
|
+
# Exit code 127 is the POSIX "command not found" signal; the regex
|
|
78
|
+
# also catches `ModuleNotFoundError` for tasks that invoke
|
|
79
|
+
# `python -m uipath` instead of the CLI.
|
|
80
|
+
cli_missing = exit_code == 127 or bool(_UIPATH_MISSING_PATTERN.search(stderr_text))
|
|
81
|
+
hint = ""
|
|
82
|
+
if cli_missing:
|
|
83
|
+
# The host's `[uipath]` extra installs `uipath` into the host
|
|
84
|
+
# venv; the sandbox runs in its own `uv` environment and must
|
|
85
|
+
# resolve `uipath` from the task's own deps.
|
|
86
|
+
hint = (
|
|
87
|
+
" — the sandbox could not resolve the `uipath` CLI. "
|
|
88
|
+
"Ensure the task's Python deps include `uipath` (the in-sandbox "
|
|
89
|
+
"dependency is separate from the host's `coder-eval[uipath]` extra)."
|
|
90
|
+
)
|
|
91
|
+
return CriterionResult(
|
|
92
|
+
criterion_type=criterion.type,
|
|
93
|
+
description=criterion.description,
|
|
94
|
+
score=0.0,
|
|
95
|
+
details=f"Command failed with exit code {exit_code}: {stderr}{hint}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Parse the output JSON file
|
|
99
|
+
try:
|
|
100
|
+
output_content = sandbox.get_file_content(output_filename)
|
|
101
|
+
results = json.loads(output_content)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return CriterionResult(
|
|
104
|
+
criterion_type=criterion.type,
|
|
105
|
+
description=criterion.description,
|
|
106
|
+
score=0.0,
|
|
107
|
+
details=f"Failed to parse {output_filename}: {e}",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not isinstance(results, dict):
|
|
111
|
+
return CriterionResult(
|
|
112
|
+
criterion_type=criterion.type,
|
|
113
|
+
description=criterion.description,
|
|
114
|
+
score=0.0,
|
|
115
|
+
details=f"Expected JSON object in {output_filename}, got {type(results).__name__}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Calculate metrics from evaluation results
|
|
119
|
+
metrics = self._calculate_metrics(results)
|
|
120
|
+
|
|
121
|
+
# Compare metrics against thresholds
|
|
122
|
+
failed_metrics = []
|
|
123
|
+
for metric_name, threshold in criterion.thresholds.items():
|
|
124
|
+
if metric_name not in metrics:
|
|
125
|
+
failed_metrics.append(f"Missing metric: {metric_name}")
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
metric_value = metrics[metric_name]
|
|
129
|
+
if not isinstance(metric_value, (int, float)):
|
|
130
|
+
failed_metrics.append(f"{metric_name}: Invalid type {type(metric_value).__name__} (expected number)")
|
|
131
|
+
elif metric_value < threshold:
|
|
132
|
+
failed_metrics.append(f"{metric_name}: {metric_value} < {threshold}")
|
|
133
|
+
|
|
134
|
+
# Calculate score based on how many thresholds are met
|
|
135
|
+
if not criterion.thresholds:
|
|
136
|
+
score = 1.0
|
|
137
|
+
else:
|
|
138
|
+
metrics_passed = len(criterion.thresholds) - len(failed_metrics)
|
|
139
|
+
score = metrics_passed / len(criterion.thresholds)
|
|
140
|
+
|
|
141
|
+
details = "All thresholds met." if not failed_metrics else f"Failed metrics: {', '.join(failed_metrics)}"
|
|
142
|
+
|
|
143
|
+
return CriterionResult(
|
|
144
|
+
criterion_type=criterion.type,
|
|
145
|
+
description=criterion.description,
|
|
146
|
+
score=score,
|
|
147
|
+
details=details,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _calculate_metrics(self, results: Any) -> dict[str, float]:
|
|
151
|
+
"""Calculate aggregated metrics from evaluation results.
|
|
152
|
+
|
|
153
|
+
Supports two formats:
|
|
154
|
+
1. Complex format with evaluationSetResults (from UiPath agents)
|
|
155
|
+
2. Simple format with flat metric keys (for testing/direct evaluation)
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
results: Parsed output.json with either evaluationSetResults or flat metrics
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dictionary with calculated metrics
|
|
162
|
+
"""
|
|
163
|
+
# Try complex format first (evaluationSetResults)
|
|
164
|
+
evaluation_set_results = results.get("evaluationSetResults", [])
|
|
165
|
+
if evaluation_set_results:
|
|
166
|
+
return self._calculate_metrics_from_evaluations(evaluation_set_results)
|
|
167
|
+
|
|
168
|
+
# Fall back to simple format (flat metrics)
|
|
169
|
+
# Keep all values including non-numeric ones, to catch type errors later
|
|
170
|
+
metrics = dict(results)
|
|
171
|
+
|
|
172
|
+
logger.debug(f"Calculated metrics: {metrics}")
|
|
173
|
+
return metrics
|
|
174
|
+
|
|
175
|
+
def _calculate_metrics_from_evaluations(self, evaluation_set_results: Any) -> dict[str, float]:
|
|
176
|
+
"""Calculate metrics from evaluationSetResults structure.
|
|
177
|
+
|
|
178
|
+
Groups metrics by evaluator ID, averages scores per evaluator (treating
|
|
179
|
+
exceptions as 0.0), then computes macro average across all evaluators.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
evaluation_set_results: List of evaluation results with evaluatorId
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Dictionary with calculated metrics, keyed by evaluator ID or "*" for macro average
|
|
186
|
+
"""
|
|
187
|
+
evaluator_scores: dict[str, list[float]] = {} # evaluator_id -> list of scores
|
|
188
|
+
|
|
189
|
+
# Iterate through all evaluations and collect scores by evaluator
|
|
190
|
+
for evaluation in evaluation_set_results:
|
|
191
|
+
evaluation_run_results = evaluation.get("evaluationRunResults", [])
|
|
192
|
+
for run_result in evaluation_run_results:
|
|
193
|
+
evaluator_id = run_result.get("evaluatorId")
|
|
194
|
+
if not evaluator_id:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
result_data = run_result.get("result", {})
|
|
198
|
+
score = result_data.get("score", 0.0)
|
|
199
|
+
|
|
200
|
+
# Treat missing/error/non-numeric scores as 0.0
|
|
201
|
+
if not isinstance(score, (int, float)):
|
|
202
|
+
score = 0.0
|
|
203
|
+
|
|
204
|
+
if evaluator_id not in evaluator_scores:
|
|
205
|
+
evaluator_scores[evaluator_id] = []
|
|
206
|
+
evaluator_scores[evaluator_id].append(score)
|
|
207
|
+
|
|
208
|
+
# Calculate per-evaluator averages
|
|
209
|
+
metrics = {}
|
|
210
|
+
evaluator_averages = []
|
|
211
|
+
for evaluator_id, scores in evaluator_scores.items():
|
|
212
|
+
avg_score = sum(scores) / len(scores) if scores else 0.0
|
|
213
|
+
metrics[evaluator_id] = avg_score
|
|
214
|
+
evaluator_averages.append(avg_score)
|
|
215
|
+
|
|
216
|
+
# Calculate macro average (average of evaluator averages)
|
|
217
|
+
if evaluator_averages:
|
|
218
|
+
metrics["*"] = sum(evaluator_averages) / len(evaluator_averages)
|
|
219
|
+
else:
|
|
220
|
+
metrics["*"] = 0.0
|
|
221
|
+
|
|
222
|
+
logger.debug(f"Calculated metrics: {metrics}")
|
|
223
|
+
return metrics
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Error categorization, retry logic, and error context capture.
|
|
2
|
+
|
|
3
|
+
This package provides production-grade error handling with:
|
|
4
|
+
- A categorized error taxonomy with per-category retry eligibility
|
|
5
|
+
- Exponential backoff retry logic with jitter
|
|
6
|
+
- Rich error context capture for debugging
|
|
7
|
+
- Actionable error tips for users
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .agent import AgentConfigError, AgentCrashError, format_timeout_reason, truncate_crash_message
|
|
11
|
+
from .budget import BudgetExceededError
|
|
12
|
+
from .judge import JudgeInfrastructureError
|
|
13
|
+
from .timeout import EvaluationTimeoutError, TaskTimeoutError, TurnTimeoutError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgentConfigError",
|
|
18
|
+
"AgentCrashError",
|
|
19
|
+
"BudgetExceededError",
|
|
20
|
+
"EvaluationTimeoutError",
|
|
21
|
+
"JudgeInfrastructureError",
|
|
22
|
+
"TaskTimeoutError",
|
|
23
|
+
"TurnTimeoutError",
|
|
24
|
+
"format_timeout_reason",
|
|
25
|
+
"truncate_crash_message",
|
|
26
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Agent-specific exceptions and crash-reason formatting helpers."""
|
|
2
|
+
|
|
3
|
+
CRASH_REASON_MAX_CHARS = 200
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_timeout_reason(timeout_seconds: float) -> str:
|
|
7
|
+
"""Canonical ``crash_reason`` string for an agent-turn timeout (integer seconds)."""
|
|
8
|
+
return f"Agent turn timed out after {timeout_seconds:.0f}s"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def truncate_crash_message(message: str, *, limit: int = CRASH_REASON_MAX_CHARS) -> str:
|
|
12
|
+
"""Cap a crash-reason string with a single Unicode ellipsis on overflow."""
|
|
13
|
+
return message if len(message) <= limit else message[:limit] + "…"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentCrashError(RuntimeError):
|
|
17
|
+
"""Mid-turn agent failure; routed to AGENT_CRASH by isinstance."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentConfigError(RuntimeError):
|
|
21
|
+
"""Agent prerequisite missing (env var, SDK path, build artifact). Non-retryable.
|
|
22
|
+
|
|
23
|
+
Routed to ``AGENT_CONFIG_ERROR`` by isinstance — preferred over substring
|
|
24
|
+
matching on a message, which can silently re-categorise a reworded
|
|
25
|
+
RuntimeError as the retryable ``AGENT_API_ERROR``.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Budget-exceeded exceptions for evaluation lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BudgetExceededError(Exception):
|
|
7
|
+
"""Raised when a RunLimits budget is exceeded between agent turns.
|
|
8
|
+
|
|
9
|
+
Carries which budget tripped and the over-budget value so the
|
|
10
|
+
orchestrator can record the status reason without re-computing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
budget_name: str,
|
|
16
|
+
*,
|
|
17
|
+
actual: float,
|
|
18
|
+
limit: float,
|
|
19
|
+
task_id: str | None = None,
|
|
20
|
+
iteration: int | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.budget_name = budget_name
|
|
23
|
+
self.actual = actual
|
|
24
|
+
self.limit = limit
|
|
25
|
+
self.task_id = task_id
|
|
26
|
+
self.iteration = iteration
|
|
27
|
+
suffix = f" (iteration {iteration})" if iteration is not None else ""
|
|
28
|
+
super().__init__(f"{budget_name} budget exceeded: {actual:g} > {limit:g}{suffix}")
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Error categories and configuration for retry logic.
|
|
2
|
+
|
|
3
|
+
This module defines error categories, retry configuration, and structured
|
|
4
|
+
error context for production-grade error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ErrorCategory(Enum):
|
|
13
|
+
"""Categorize errors for retry logic and reporting.
|
|
14
|
+
|
|
15
|
+
Categories are grouped by component:
|
|
16
|
+
- Agent errors (AGENT_*)
|
|
17
|
+
- Sandbox errors (SANDBOX_*, VENV_*, PACKAGE_*, etc.)
|
|
18
|
+
- Evaluator errors (CRITERION_*, LLM_REVIEWER_*)
|
|
19
|
+
- Task errors (TASK_*)
|
|
20
|
+
- System errors (DISK_FULL, OUT_OF_MEMORY)
|
|
21
|
+
|
|
22
|
+
Each category has an associated retry policy in RETRY_CONFIG.
|
|
23
|
+
|
|
24
|
+
NB: each ``value`` is persisted verbatim into task.json's
|
|
25
|
+
``error_details.error_category`` — a cross-repo contract surface read by the
|
|
26
|
+
external eval-runner/dashboard. Adding/renaming/removing a member is a
|
|
27
|
+
contract change: update the pinned set in
|
|
28
|
+
``tests/test_error_handling.py::test_error_category_values_are_a_pinned_contract``
|
|
29
|
+
and flag in the PR that a new persisted value now appears downstream (which
|
|
30
|
+
needs a display label/icon). FinalStatus bucketing is unchanged, so the
|
|
31
|
+
blast radius is display/filter only.
|
|
32
|
+
|
|
33
|
+
Removed 2026-06-22: ``TESTS_FAILED`` and ``SANDBOX_COMMAND_ERROR`` — neither
|
|
34
|
+
had a producing ``categorize_error`` path or a downstream consumer. The
|
|
35
|
+
liveness test in ``tests/test_error_handling.py`` now keeps every member
|
|
36
|
+
producible, so a future dead member fails CI.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Agent Errors - Communication and execution errors
|
|
40
|
+
AGENT_TIMEOUT = "agent_timeout" # Agent exceeded time limit (NOT retryable)
|
|
41
|
+
AGENT_API_ERROR = "agent_api_error" # API connection/network error (retryable)
|
|
42
|
+
AGENT_RATE_LIMIT = "agent_rate_limit" # API rate limit (retryable with long delay)
|
|
43
|
+
AGENT_AUTH_ERROR = "agent_auth_error" # Invalid API key (NOT retryable)
|
|
44
|
+
AGENT_BILLING_ERROR = "agent_billing_error" # Insufficient credits/billing issue (NOT retryable)
|
|
45
|
+
AGENT_CRASH = "agent_crash" # Agent process crashed (retryable — CLI crashes are often transient)
|
|
46
|
+
AGENT_INVALID_OUTPUT = "agent_invalid_output" # Malformed response (NOT retryable)
|
|
47
|
+
AGENT_CONFIG_ERROR = "agent_config_error" # Missing prerequisite/misconfiguration (NOT retryable)
|
|
48
|
+
|
|
49
|
+
# Sandbox Errors - Environment setup and execution
|
|
50
|
+
SANDBOX_SETUP_ERROR = "sandbox_setup_error" # Failed to create sandbox (retryable)
|
|
51
|
+
VENV_CREATION_ERROR = "venv_creation_error" # Virtual env creation failed (retryable)
|
|
52
|
+
PACKAGE_INSTALL_ERROR = "package_install_error" # pip install failed (retryable)
|
|
53
|
+
TEMPLATE_COPY_ERROR = "template_copy_error" # Template copy failed (retryable)
|
|
54
|
+
GIT_CLONE_ERROR = "git_clone_error" # Git clone failed (retryable)
|
|
55
|
+
|
|
56
|
+
# Criterion/Evaluator Errors
|
|
57
|
+
CRITERION_CHECK_ERROR = "criterion_check_error" # Error checking criterion (NOT retryable)
|
|
58
|
+
|
|
59
|
+
# Task Errors - Task definition and loading
|
|
60
|
+
TASK_NOT_FOUND = "task_not_found" # Task file doesn't exist (NOT retryable)
|
|
61
|
+
TASK_INVALID = "task_invalid" # Malformed task.yaml (NOT retryable)
|
|
62
|
+
|
|
63
|
+
# System Errors - Resource exhaustion
|
|
64
|
+
DISK_FULL = "disk_full" # No disk space (NOT retryable)
|
|
65
|
+
OUT_OF_MEMORY = "out_of_memory" # OOM error (NOT retryable)
|
|
66
|
+
|
|
67
|
+
# Budget governance — RunLimits caps tripped (NOT retryable — retrying compounds the breach)
|
|
68
|
+
BUDGET_EXCEEDED = "budget_exceeded"
|
|
69
|
+
|
|
70
|
+
# Unknown
|
|
71
|
+
UNKNOWN = "unknown" # Uncategorized error (NOT retryable)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RetryConfig(BaseModel):
|
|
75
|
+
"""Configuration for retry behavior.
|
|
76
|
+
|
|
77
|
+
Defines how many times to retry and with what backoff strategy.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
max_retries: int = Field(default=0, ge=0, description="Maximum retry attempts (0 = no retry)")
|
|
81
|
+
backoff_multiplier: float = Field(default=2.0, gt=0, description="Exponential backoff multiplier")
|
|
82
|
+
initial_delay: float = Field(default=5.0, gt=0, description="Initial delay in seconds")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ErrorContext(BaseModel):
|
|
86
|
+
"""Structured error context for reporting and debugging.
|
|
87
|
+
|
|
88
|
+
This model provides type-safe error context with validation.
|
|
89
|
+
Use error_context_to_dict() to convert to dict for EvaluationResult.error_details.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
error_category: str = Field(description="Error category value")
|
|
93
|
+
error_message: str = Field(description="Exception message")
|
|
94
|
+
stack_trace: str = Field(description="Full stack trace")
|
|
95
|
+
task_id: str = Field(description="Task identifier")
|
|
96
|
+
component: str | None = Field(default=None, description="Component that failed")
|
|
97
|
+
agent_name: str | None = Field(default=None, description="Agent name if applicable")
|
|
98
|
+
attempt_number: int = Field(ge=1, description="Attempt number (1-indexed)")
|
|
99
|
+
is_retryable: bool = Field(description="Whether error is retryable")
|
|
100
|
+
retry_delay_seconds: float = Field(ge=0.0, description="Delay before next retry")
|
|
101
|
+
disk_usage_gb: float | None = Field(default=None, description="Disk usage in GB")
|
|
102
|
+
memory_usage_gb: float | None = Field(default=None, description="Memory usage in GB")
|
|
103
|
+
agent_stdout: str | None = Field(default=None, description="Agent stdout (truncated)")
|
|
104
|
+
agent_stderr: str | None = Field(default=None, description="Agent stderr (truncated)")
|
|
105
|
+
timestamp: str = Field(description="ISO timestamp when error occurred")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Retry configuration per error category
|
|
109
|
+
# Non-retryable errors have max_retries=0 (default)
|
|
110
|
+
RETRY_CONFIG: dict[ErrorCategory, RetryConfig] = {
|
|
111
|
+
# Agent errors - API issues are retryable
|
|
112
|
+
ErrorCategory.AGENT_API_ERROR: RetryConfig(
|
|
113
|
+
max_retries=3,
|
|
114
|
+
backoff_multiplier=2.0,
|
|
115
|
+
initial_delay=5.0,
|
|
116
|
+
),
|
|
117
|
+
ErrorCategory.AGENT_RATE_LIMIT: RetryConfig(
|
|
118
|
+
max_retries=5,
|
|
119
|
+
backoff_multiplier=3.0,
|
|
120
|
+
initial_delay=60.0, # 1 minute initial delay
|
|
121
|
+
),
|
|
122
|
+
ErrorCategory.AGENT_CRASH: RetryConfig(
|
|
123
|
+
max_retries=2,
|
|
124
|
+
backoff_multiplier=2.0,
|
|
125
|
+
initial_delay=5.0,
|
|
126
|
+
),
|
|
127
|
+
# Sandbox errors - transient failures are retryable
|
|
128
|
+
ErrorCategory.SANDBOX_SETUP_ERROR: RetryConfig(
|
|
129
|
+
max_retries=2,
|
|
130
|
+
backoff_multiplier=1.5,
|
|
131
|
+
initial_delay=10.0,
|
|
132
|
+
),
|
|
133
|
+
ErrorCategory.PACKAGE_INSTALL_ERROR: RetryConfig(
|
|
134
|
+
max_retries=2,
|
|
135
|
+
backoff_multiplier=2.0,
|
|
136
|
+
initial_delay=5.0,
|
|
137
|
+
),
|
|
138
|
+
ErrorCategory.VENV_CREATION_ERROR: RetryConfig(
|
|
139
|
+
max_retries=2,
|
|
140
|
+
backoff_multiplier=2.0,
|
|
141
|
+
initial_delay=5.0,
|
|
142
|
+
),
|
|
143
|
+
ErrorCategory.TEMPLATE_COPY_ERROR: RetryConfig(
|
|
144
|
+
max_retries=2,
|
|
145
|
+
backoff_multiplier=1.5,
|
|
146
|
+
initial_delay=5.0,
|
|
147
|
+
),
|
|
148
|
+
ErrorCategory.GIT_CLONE_ERROR: RetryConfig(
|
|
149
|
+
max_retries=3,
|
|
150
|
+
backoff_multiplier=2.0,
|
|
151
|
+
initial_delay=10.0,
|
|
152
|
+
),
|
|
153
|
+
# All other errors are NOT retryable (max_retries=0 by default)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Error tip system for better UX
|
|
158
|
+
ERROR_TIPS = {
|
|
159
|
+
ErrorCategory.AGENT_TIMEOUT: (
|
|
160
|
+
"Agent or task timed out. Consider increasing the limit "
|
|
161
|
+
"(-D run_limits.turn_timeout=… or -D run_limits.task_timeout=…), "
|
|
162
|
+
"or simplifying the task to require fewer iterations."
|
|
163
|
+
),
|
|
164
|
+
ErrorCategory.AGENT_RATE_LIMIT: (
|
|
165
|
+
"Rate limit hit. Consider using --max-parallel=1 to reduce concurrent API calls, "
|
|
166
|
+
"or check your API key quota at your provider's dashboard."
|
|
167
|
+
),
|
|
168
|
+
ErrorCategory.AGENT_AUTH_ERROR: (
|
|
169
|
+
"Authentication failed. Verify your ANTHROPIC_API_KEY is set correctly in .env file and has not expired."
|
|
170
|
+
),
|
|
171
|
+
ErrorCategory.PACKAGE_INSTALL_ERROR: (
|
|
172
|
+
"Package installation failed. Check your network connection, or try clearing pip cache with 'pip cache purge'."
|
|
173
|
+
),
|
|
174
|
+
ErrorCategory.DISK_FULL: (
|
|
175
|
+
"Disk full. Run 'coder-eval health' to check usage, then 'coder-eval gc' to free space, "
|
|
176
|
+
"or delete old runs manually from the runs/ directory."
|
|
177
|
+
),
|
|
178
|
+
ErrorCategory.VENV_CREATION_ERROR: (
|
|
179
|
+
"Virtual environment creation failed. Ensure Python 3.13+ is installed and accessible."
|
|
180
|
+
),
|
|
181
|
+
ErrorCategory.GIT_CLONE_ERROR: (
|
|
182
|
+
"Git clone failed. Check your network connection and verify the repository URL is accessible."
|
|
183
|
+
),
|
|
184
|
+
ErrorCategory.AGENT_API_ERROR: (
|
|
185
|
+
"API connection error. Check your network connection and verify the API endpoint is accessible."
|
|
186
|
+
),
|
|
187
|
+
ErrorCategory.AGENT_BILLING_ERROR: (
|
|
188
|
+
"Insufficient API credits or billing issue. Check your account balance and billing settings "
|
|
189
|
+
"at your provider's dashboard."
|
|
190
|
+
),
|
|
191
|
+
ErrorCategory.AGENT_CRASH: (
|
|
192
|
+
"Claude CLI process exited unexpectedly. Common causes: insufficient API credits, "
|
|
193
|
+
"invalid API key, or network issues. Run 'claude --version' to verify the CLI works, "
|
|
194
|
+
"then try running your prompt directly with 'claude' to see the actual error."
|
|
195
|
+
),
|
|
196
|
+
ErrorCategory.BUDGET_EXCEEDED: (
|
|
197
|
+
"RunLimits budget exceeded. Raise the cap in the task's run_limits block, or simplify "
|
|
198
|
+
"the prompt to use fewer tokens."
|
|
199
|
+
),
|
|
200
|
+
ErrorCategory.AGENT_CONFIG_ERROR: (
|
|
201
|
+
"Agent configuration error — a required environment variable, SDK path, or "
|
|
202
|
+
"build artifact is missing. Check the error message for the specific prerequisite, "
|
|
203
|
+
"and see the agent plugin's setup docs."
|
|
204
|
+
),
|
|
205
|
+
}
|