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,942 @@
|
|
|
1
|
+
"""Success criteria models for task evaluation."""
|
|
2
|
+
|
|
3
|
+
# Each concrete criterion narrows the base ``type: str`` to its ``Literal[...]`` tag
|
|
4
|
+
# (the standard pydantic discriminated-union pattern). Pyright treats a Literal
|
|
5
|
+
# subtype override of a mutable str field as variable-invariant-incompatible, but
|
|
6
|
+
# the narrowing is exactly the intended discriminator behaviour here.
|
|
7
|
+
# pyright: reportIncompatibleVariableOverride=false
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC
|
|
12
|
+
from typing import Annotated, Any, ClassVar, Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
15
|
+
|
|
16
|
+
from coder_eval.models.agent_config import AgentConfig, ClaudeCodeAgentConfig, parse_agent_config
|
|
17
|
+
from coder_eval.models.enums import AgentKind
|
|
18
|
+
from coder_eval.models.judge_defaults import DEFAULT_JUDGE_MODEL
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# SECURITY: ignore_patterns floor. The judge's working directory is a copy of
|
|
22
|
+
# the agent's sandbox; if the agent dropped these in, they'd otherwise be
|
|
23
|
+
# copied across and either (a) cause the SDK to load hooks / MCP servers from
|
|
24
|
+
# the main agent's settings (despite ``setting_sources=[]``, the agent ignore
|
|
25
|
+
# patterns also gate what shutil.copytree pulls in) or (b) collide with the
|
|
26
|
+
# reference mount point. The floor is enforced UNCONDITIONALLY in
|
|
27
|
+
# ``criteria/agent_judge.py::_build_agent_config``, even when the user
|
|
28
|
+
# supplied their own ``agent.ignore_patterns`` — author convenience does not
|
|
29
|
+
# override the judge's safety floor. Single source of truth: importing this
|
|
30
|
+
# constant from both call-sites keeps the model defaults and the checker
|
|
31
|
+
# floor in sync.
|
|
32
|
+
JUDGE_SECURITY_IGNORE_FLOOR: tuple[str, ...] = (".claude", ".mcp.json", "_reference")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _reject_removed_verdict_channel(data: Any) -> Any:
|
|
36
|
+
"""Raise with a migration message when YAML still carries the removed ``verdict_channel`` key.
|
|
37
|
+
|
|
38
|
+
Shared between ``LLMJudgeCriterion`` and ``AgentJudgeCriterion`` so the error
|
|
39
|
+
string stays in lockstep. Runs in ``mode="before"`` so the migration message
|
|
40
|
+
wins over the generic ``extra="forbid"`` "Extra inputs are not permitted"
|
|
41
|
+
error.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(data, dict) and "verdict_channel" in data:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"verdict_channel field was removed on 2026-05-21; the submit_verdict "
|
|
46
|
+
+ "tool channel is the only path. Delete this field from your task YAML."
|
|
47
|
+
)
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _default_judge_agent_config() -> ClaudeCodeAgentConfig:
|
|
52
|
+
"""Build the default judge AgentConfig.
|
|
53
|
+
|
|
54
|
+
The returned ``ignore_patterns`` extend ``JUDGE_SECURITY_IGNORE_FLOOR``
|
|
55
|
+
with developer-noise dirs (``.git``, ``node_modules``, ...). The security
|
|
56
|
+
trio is appended via the shared constant so it cannot drift from the
|
|
57
|
+
floor enforced in ``_build_agent_config``.
|
|
58
|
+
"""
|
|
59
|
+
# Developer-noise dirs first, security floor (single source of truth) last.
|
|
60
|
+
developer_noise = [".git", "node_modules", "__pycache__", ".venv"]
|
|
61
|
+
cfg = parse_agent_config(
|
|
62
|
+
type=AgentKind.CLAUDE_CODE,
|
|
63
|
+
model="claude-sonnet-4-6",
|
|
64
|
+
permission_mode="bypassPermissions",
|
|
65
|
+
allowed_tools=["Bash", "Read", "Glob", "Grep"],
|
|
66
|
+
ignore_patterns=[*developer_noise, *JUDGE_SECURITY_IGNORE_FLOOR],
|
|
67
|
+
)
|
|
68
|
+
assert isinstance(cfg, ClaudeCodeAgentConfig)
|
|
69
|
+
return cfg
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BaseSuccessCriterion(BaseModel, ABC):
|
|
73
|
+
"""Base class for success criteria - pure data container.
|
|
74
|
+
|
|
75
|
+
Success criteria are data models that describe WHAT to check,
|
|
76
|
+
but not HOW to check it. The checking logic belongs in SuccessChecker.
|
|
77
|
+
|
|
78
|
+
This separation provides:
|
|
79
|
+
- Models focus on data + validation (Pydantic's strength)
|
|
80
|
+
- SuccessChecker focuses on business logic
|
|
81
|
+
- Easier testing with mocked sandboxes
|
|
82
|
+
- Better separation of concerns
|
|
83
|
+
- No circular dependency with Sandbox class
|
|
84
|
+
|
|
85
|
+
``extra='forbid'`` here propagates to every concrete criterion subclass via
|
|
86
|
+
pydantic's model_config inheritance. A typo in any criterion-specific field
|
|
87
|
+
in a task YAML (e.g. ``patten:`` instead of ``pattern:``) surfaces as an
|
|
88
|
+
explicit ``Extra inputs are not permitted`` error rather than being silently
|
|
89
|
+
dropped on the floor.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(extra="forbid")
|
|
93
|
+
|
|
94
|
+
# Discriminator; each concrete criterion narrows this to its Literal tag.
|
|
95
|
+
type: str
|
|
96
|
+
|
|
97
|
+
description: str = Field(description="Human-readable description of what this criterion checks")
|
|
98
|
+
|
|
99
|
+
weight: float = Field(
|
|
100
|
+
default=1.0,
|
|
101
|
+
ge=0.0,
|
|
102
|
+
description=(
|
|
103
|
+
"Relative importance of this criterion in the weighted score (default: 1.0). Set to 0 to "
|
|
104
|
+
"exclude it from the weighted SCORE -- useful for informational or side-effect checks "
|
|
105
|
+
"(e.g. a setup command). NOTE: weight=0 excludes from the score but NOT from the pass/fail "
|
|
106
|
+
"gate -- a criterion scoring below its pass_threshold still flips the task to FAILURE "
|
|
107
|
+
"regardless of weight. To make a criterion truly non-gating, also set pass_threshold=0."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
pass_threshold: float = Field(
|
|
112
|
+
default=0.9, ge=0.0, le=1.0, description="Minimum score required to pass (default: 0.9 = 90%)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
suite_thresholds: dict[str, float] | None = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
description=(
|
|
118
|
+
"Across-row thresholds as {metric_name: minimum}. Only valid on tasks that declare a dataset:. "
|
|
119
|
+
"The criterion passes at the suite level iff every listed metric meets its minimum. "
|
|
120
|
+
"Metric names come from the criterion's aggregate() output (e.g. 'accuracy', 'f1.macro', "
|
|
121
|
+
"'recall.positive' for classification_match)."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
requires_agent: ClassVar[bool] = False
|
|
126
|
+
"""True if this criterion requires agent turn records to evaluate correctly."""
|
|
127
|
+
|
|
128
|
+
def model_post_init(self, context: Any, /) -> None:
|
|
129
|
+
# Pin the discriminator tag into model_fields_set so it survives
|
|
130
|
+
# model_dump(exclude_unset=True) → model_validate() round-trips even for
|
|
131
|
+
# directly-constructed criteria (where the tag comes from the Literal
|
|
132
|
+
# default, not the caller). Without this, exclude_unset drops the tag and
|
|
133
|
+
# the discriminated union rejects the dump with union_tag_not_found.
|
|
134
|
+
self.__pydantic_fields_set__.add("type")
|
|
135
|
+
|
|
136
|
+
# Business logic (check operations) moved to SuccessChecker in evaluator.py
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class FileExistsCriterion(BaseSuccessCriterion):
|
|
140
|
+
"""Check if a file exists at the specified path.
|
|
141
|
+
|
|
142
|
+
Pure data model - checking logic in SuccessChecker._check_file_exists()
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
type: Literal["file_exists"] = "file_exists"
|
|
146
|
+
path: str = Field(description="Path to the file that must exist")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FileContainsCriterion(BaseSuccessCriterion):
|
|
150
|
+
"""Check if a file contains specific strings.
|
|
151
|
+
|
|
152
|
+
Pure data model - checking logic in SuccessChecker._check_file_contains()
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
type: Literal["file_contains"] = "file_contains"
|
|
156
|
+
path: str = Field(description="Path to the file to check")
|
|
157
|
+
includes: list[str] = Field(description="List of strings that must be present in the file")
|
|
158
|
+
excludes: list[str] | None = Field(default=None, description="List of strings that must NOT be present in the file")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RunCommandCriterion(BaseSuccessCriterion):
|
|
162
|
+
"""Check if a command runs successfully, with optional stdout matching.
|
|
163
|
+
|
|
164
|
+
When ``expected_stdout`` is set, the criterion also compares the command's
|
|
165
|
+
stdout against the expected value using the mode specified by ``stdout_match``.
|
|
166
|
+
This subsumes the former ``program_stdout_equals`` criterion.
|
|
167
|
+
|
|
168
|
+
Pure data model - checking logic in RunCommandChecker._check_impl()
|
|
169
|
+
|
|
170
|
+
Example YAML:
|
|
171
|
+
success_criteria:
|
|
172
|
+
# Simple exit-code check (original behavior)
|
|
173
|
+
- type: "run_command"
|
|
174
|
+
command: "python app.py"
|
|
175
|
+
description: "Script must run successfully"
|
|
176
|
+
|
|
177
|
+
# With stdout matching (replaces program_stdout_equals)
|
|
178
|
+
- type: "run_command"
|
|
179
|
+
command: "python hello.py"
|
|
180
|
+
expected_stdout: "Hello, World!"
|
|
181
|
+
stdout_match: "exact"
|
|
182
|
+
description: "Script must output the correct text"
|
|
183
|
+
|
|
184
|
+
# Continuous scoring from stdout (first line must be a float 0.0-1.0)
|
|
185
|
+
- type: "run_command"
|
|
186
|
+
command: "python score.py"
|
|
187
|
+
score_from_stdout: true
|
|
188
|
+
description: "Similarity score from scoring script"
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
type: Literal["run_command"] = "run_command"
|
|
192
|
+
command: str = Field(description="Command to execute")
|
|
193
|
+
timeout: int = Field(default=30, description="Timeout in seconds")
|
|
194
|
+
expected_exit_code: int = Field(default=0, description="Expected exit code")
|
|
195
|
+
expected_stdout: str | None = Field(
|
|
196
|
+
default=None, description="Expected stdout content. When set, stdout is also checked."
|
|
197
|
+
)
|
|
198
|
+
stdout_match: Literal["exact", "contains", "regex"] = Field(
|
|
199
|
+
default="exact",
|
|
200
|
+
description="How to match stdout: 'exact' (stripped), 'contains' (substring), 'regex' (pattern)",
|
|
201
|
+
)
|
|
202
|
+
score_from_stdout: bool = Field(
|
|
203
|
+
default=False,
|
|
204
|
+
description=(
|
|
205
|
+
"When true, read a float score (0.0-1.0) from the first line of stdout. "
|
|
206
|
+
"Remaining lines are captured as details. Non-zero exit code or parse failure -> score 0.0. "
|
|
207
|
+
"Mutually exclusive with expected_stdout."
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@model_validator(mode="after")
|
|
212
|
+
def check_score_from_stdout_exclusivity(self) -> RunCommandCriterion:
|
|
213
|
+
if self.score_from_stdout and self.expected_stdout is not None:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
"'score_from_stdout' and 'expected_stdout' are mutually exclusive. "
|
|
216
|
+
+ "Use 'score_from_stdout: true' for continuous float scoring, "
|
|
217
|
+
+ "or 'expected_stdout' for exact/contains/regex string matching."
|
|
218
|
+
)
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class FileMatchesRegexCriterion(BaseSuccessCriterion):
|
|
223
|
+
"""Check if file content matches a regex pattern.
|
|
224
|
+
|
|
225
|
+
Pure data model - checking logic in SuccessChecker._check_file_matches_regex()
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
type: Literal["file_matches_regex"] = "file_matches_regex"
|
|
229
|
+
path: str = Field(description="Path to the file to check")
|
|
230
|
+
pattern: str = Field(description="Regex pattern that must match somewhere in the file")
|
|
231
|
+
must_match: bool = Field(default=True, description="If True, pattern must match; if False, pattern must NOT match")
|
|
232
|
+
flags: int = Field(default=0, description="Regex flags (e.g., re.IGNORECASE=2, re.MULTILINE=8, re.DOTALL=16)")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class RegexPattern(BaseModel):
|
|
236
|
+
"""A single regex pattern check within FileCheckCriterion."""
|
|
237
|
+
|
|
238
|
+
model_config = ConfigDict(extra="forbid")
|
|
239
|
+
|
|
240
|
+
pattern: str = Field(description="Regex pattern to match against file content")
|
|
241
|
+
must_match: bool = Field(default=True, description="If True, pattern must match; if False, pattern must NOT match")
|
|
242
|
+
flags: int = Field(default=0, description="Regex flags (e.g., re.IGNORECASE=2, re.MULTILINE=8, re.DOTALL=16)")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_OPERATORS_REQUIRING_EXPECTED = frozenset(
|
|
246
|
+
{
|
|
247
|
+
"equals",
|
|
248
|
+
"not_equals",
|
|
249
|
+
"contains",
|
|
250
|
+
"gt",
|
|
251
|
+
"gte",
|
|
252
|
+
"lt",
|
|
253
|
+
"lte",
|
|
254
|
+
"type",
|
|
255
|
+
"regex",
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# 8 admits common complete words while still catching truncated fragments;
|
|
260
|
+
# see reject_brittle_substring_contains docstring for full rationale.
|
|
261
|
+
_MIN_CONTAINS_LITERAL_LEN = 8
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class JMESPathAssertion(BaseModel):
|
|
265
|
+
"""A single JMESPath assertion within JsonCheckCriterion."""
|
|
266
|
+
|
|
267
|
+
model_config = ConfigDict(extra="forbid")
|
|
268
|
+
|
|
269
|
+
expression: str = Field(description="JMESPath expression to evaluate against the parsed JSON")
|
|
270
|
+
operator: Literal["equals", "not_equals", "contains", "gt", "gte", "lt", "lte", "type", "regex", "exists"] = Field(
|
|
271
|
+
default="equals", description="Comparison operator (default: 'equals')"
|
|
272
|
+
)
|
|
273
|
+
expected: Any = Field(default=None, description="Expected value. Not required when operator is 'exists'.")
|
|
274
|
+
|
|
275
|
+
@model_validator(mode="after")
|
|
276
|
+
def validate_expected_for_operator(self) -> JMESPathAssertion:
|
|
277
|
+
"""Enforce that 'expected' is provided for operators that require it.
|
|
278
|
+
|
|
279
|
+
Uses model_fields_set to distinguish 'expected' not provided from
|
|
280
|
+
explicitly set to None (valid JSON null).
|
|
281
|
+
"""
|
|
282
|
+
expected_provided = "expected" in self.model_fields_set
|
|
283
|
+
if self.operator in _OPERATORS_REQUIRING_EXPECTED and not expected_provided:
|
|
284
|
+
raise ValueError(f"'expected' is required when operator is '{self.operator}'")
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
@model_validator(mode="after")
|
|
288
|
+
def reject_brittle_substring_contains(self) -> JMESPathAssertion:
|
|
289
|
+
"""Reject `operator: contains` with a literal `expected` shorter than 8 stripped characters.
|
|
290
|
+
|
|
291
|
+
A short `contains` substring against an agent-paraphrased JSON value
|
|
292
|
+
flakes when the agent's wording shifts (e.g. `"scalat"` chosen as
|
|
293
|
+
a fragment of "escalation" misses when the agent writes "review"
|
|
294
|
+
instead). Whitespace-only literals (e.g. `" "`) are equally
|
|
295
|
+
brittle — they match almost any non-empty string — so length is
|
|
296
|
+
measured after `.strip()`.
|
|
297
|
+
|
|
298
|
+
Scope is deliberately narrow:
|
|
299
|
+
- `regex` and `equals` are not validated: those operators are explicit
|
|
300
|
+
author signals that brittle short matching is intentional (e.g. an
|
|
301
|
+
anchored regex or a canonical machine name).
|
|
302
|
+
- Non-string `expected` (numeric, list) is not validated: `contains`
|
|
303
|
+
against a list is exact element equality, not substring matching.
|
|
304
|
+
"""
|
|
305
|
+
if (
|
|
306
|
+
self.operator == "contains"
|
|
307
|
+
and isinstance(self.expected, str)
|
|
308
|
+
and len(self.expected.strip()) < _MIN_CONTAINS_LITERAL_LEN
|
|
309
|
+
):
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Brittle 'contains' assertion: expected literal {self.expected!r}"
|
|
312
|
+
+ f" has {len(self.expected.strip())} non-whitespace characters; short"
|
|
313
|
+
+ " substrings of paraphrased JSON values flake when agent wording varies."
|
|
314
|
+
+ " Use operator='regex' with an anchored pattern, or"
|
|
315
|
+
+ " operator='equals' with a canonical machine name."
|
|
316
|
+
)
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class JsonCheckCriterion(BaseSuccessCriterion):
|
|
321
|
+
"""Validate a JSON file: existence, parseability, schema conformance, and JMESPath assertions.
|
|
322
|
+
|
|
323
|
+
Fractional scoring. File missing or invalid JSON -> 0.0.
|
|
324
|
+
Only active categories (schema, assertions) contribute to the average.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
type: Literal["json_check"] = "json_check"
|
|
328
|
+
path: str = Field(description="Path to the JSON file (relative to sandbox root)")
|
|
329
|
+
json_schema: str | None = Field(default=None, description="Path to JSON Schema file (relative to sandbox root)")
|
|
330
|
+
assertions: list[JMESPathAssertion] = Field(
|
|
331
|
+
default_factory=list, description="JMESPath assertions to evaluate against the parsed JSON"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class FileCheckCriterion(BaseSuccessCriterion):
|
|
336
|
+
"""Unified file check: existence + string includes/excludes + regex patterns.
|
|
337
|
+
|
|
338
|
+
Existence is implicit — if the file doesn't exist, all checks fail (score 0.0).
|
|
339
|
+
All other fields are optional: if none are specified, this is a pure existence check.
|
|
340
|
+
|
|
341
|
+
Scoring: fractional, computed as the average of active sub-check scores
|
|
342
|
+
(includes score, excludes score, and patterns score). Only categories with
|
|
343
|
+
non-empty lists contribute to the average, preventing score inflation.
|
|
344
|
+
|
|
345
|
+
Pure data model - checking logic in FileCheckChecker._check_impl()
|
|
346
|
+
|
|
347
|
+
Example YAML:
|
|
348
|
+
success_criteria:
|
|
349
|
+
- type: "file_check"
|
|
350
|
+
path: "main.py"
|
|
351
|
+
includes: ["from uipath import UiPath"]
|
|
352
|
+
excludes: ["import os"]
|
|
353
|
+
patterns:
|
|
354
|
+
- pattern: "def main\\(.*\\):"
|
|
355
|
+
must_match: true
|
|
356
|
+
description: "main.py exists with correct imports and structure"
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
type: Literal["file_check"] = "file_check"
|
|
360
|
+
path: str = Field(description="Path to the file to check (relative to sandbox root)")
|
|
361
|
+
includes: list[str] = Field(default_factory=list, description="Strings that must be present in the file")
|
|
362
|
+
excludes: list[str] = Field(default_factory=list, description="Strings that must NOT be present in the file")
|
|
363
|
+
patterns: list[RegexPattern] = Field(
|
|
364
|
+
default_factory=list, description="Regex patterns to check against file content"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class ReferenceComparisonCriterion(BaseSuccessCriterion):
|
|
369
|
+
"""Compare agent code against reference solution.
|
|
370
|
+
|
|
371
|
+
Uses the top-level `reference` block from TaskDefinition.
|
|
372
|
+
The reference code is loaded by the orchestrator and passed to the success checker.
|
|
373
|
+
|
|
374
|
+
Pure data model - checking logic in SuccessChecker._check_reference_comparison()
|
|
375
|
+
|
|
376
|
+
Example YAML:
|
|
377
|
+
success_criteria:
|
|
378
|
+
- type: "reference_comparison"
|
|
379
|
+
description: "Code structure matches reference"
|
|
380
|
+
agent_file: "solution.py"
|
|
381
|
+
comparison_method: "ast"
|
|
382
|
+
similarity_threshold: 0.8
|
|
383
|
+
weight: 1.0
|
|
384
|
+
pass_threshold: 0.8
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
requires_agent: ClassVar[bool] = True
|
|
388
|
+
|
|
389
|
+
type: Literal["reference_comparison"] = "reference_comparison"
|
|
390
|
+
|
|
391
|
+
# Required fields
|
|
392
|
+
agent_file: str = Field(description="Path to agent's generated file (relative to sandbox root)")
|
|
393
|
+
|
|
394
|
+
comparison_method: Literal["ast", "token", "complexity"] = Field(
|
|
395
|
+
default="ast",
|
|
396
|
+
description="Method for comparing code: 'ast' (structure), 'token' (text), 'complexity' (metrics)",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
similarity_threshold: float = Field(
|
|
400
|
+
default=0.8, ge=0.0, le=1.0, description="Minimum similarity score to pass (0.0-1.0)"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
@model_validator(mode="after")
|
|
404
|
+
def align_threshold_to_similarity(self) -> ReferenceComparisonCriterion:
|
|
405
|
+
"""Align pass_threshold to similarity_threshold for clarity.
|
|
406
|
+
|
|
407
|
+
Since similarity_threshold is the domain-specific field, it takes precedence.
|
|
408
|
+
"""
|
|
409
|
+
self.pass_threshold = self.similarity_threshold
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class CommandsEfficiencyCriterion(BaseSuccessCriterion):
|
|
414
|
+
"""Score agent tool-call efficiency relative to an expected budget.
|
|
415
|
+
|
|
416
|
+
Score = expected / max(actual, expected). Yields 1.0 at or under budget.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
requires_agent: ClassVar[bool] = True
|
|
420
|
+
type: Literal["commands_efficiency"] = "commands_efficiency"
|
|
421
|
+
expected_commands: int = Field(ge=1, description="Expected number of tool commands to complete the task")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class CommandExecutedCriterion(BaseSuccessCriterion):
|
|
425
|
+
"""Check whether the agent executed specific commands/tools.
|
|
426
|
+
|
|
427
|
+
Inspects CommandTelemetry records from TurnRecord.commands to verify
|
|
428
|
+
that the agent used specific tools or commands during evaluation.
|
|
429
|
+
|
|
430
|
+
Pure data model - checking logic in CommandExecutedChecker._check_impl()
|
|
431
|
+
|
|
432
|
+
Example YAML (positive — must run at least once):
|
|
433
|
+
success_criteria:
|
|
434
|
+
- type: "command_executed"
|
|
435
|
+
description: "Agent used curl to fetch weather"
|
|
436
|
+
tool_name: "Bash"
|
|
437
|
+
command_pattern: "curl.*wttr\\.in"
|
|
438
|
+
min_count: 1
|
|
439
|
+
require_success: true
|
|
440
|
+
|
|
441
|
+
Example YAML (negative — must NOT run; uses ``min_count: 0`` + ``max_count: 0``):
|
|
442
|
+
success_criteria:
|
|
443
|
+
- type: "command_executed"
|
|
444
|
+
description: "Agent did NOT call the retired endpoint"
|
|
445
|
+
tool_name: "Bash"
|
|
446
|
+
command_pattern: "uip\\s+or\\s+users\\s+list"
|
|
447
|
+
min_count: 0
|
|
448
|
+
max_count: 0
|
|
449
|
+
|
|
450
|
+
Example YAML (bounded range — between 3 and 5 calls):
|
|
451
|
+
success_criteria:
|
|
452
|
+
- type: "command_executed"
|
|
453
|
+
description: "Agent retried the right number of times"
|
|
454
|
+
tool_name: "Bash"
|
|
455
|
+
command_pattern: "curl.*api\\.example\\.com"
|
|
456
|
+
min_count: 3
|
|
457
|
+
max_count: 5
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
requires_agent: ClassVar[bool] = True
|
|
461
|
+
|
|
462
|
+
type: Literal["command_executed"] = "command_executed"
|
|
463
|
+
tool_name: str | None = Field(default=None, description="Tool name filter (e.g., 'Bash'). None = any tool.")
|
|
464
|
+
command_pattern: str | None = Field(
|
|
465
|
+
default=None, description="Regex to match command parameters. None = any command."
|
|
466
|
+
)
|
|
467
|
+
min_count: int = Field(
|
|
468
|
+
default=1,
|
|
469
|
+
ge=0,
|
|
470
|
+
description=(
|
|
471
|
+
"Minimum matching commands required. ``0`` allows the criterion to "
|
|
472
|
+
"pass when no commands match (combine with ``max_count: 0`` to "
|
|
473
|
+
"express ``must NOT match``)."
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
max_count: int | None = Field(
|
|
477
|
+
default=None,
|
|
478
|
+
ge=0,
|
|
479
|
+
description=(
|
|
480
|
+
"Optional maximum matching commands allowed (inclusive). ``None`` "
|
|
481
|
+
"means no upper bound — the current default. When set, the criterion "
|
|
482
|
+
"passes iff ``min_count <= match_count <= max_count``."
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
require_success: bool = Field(default=False, description="If True, only count successful commands.")
|
|
486
|
+
exclude_pattern: str | None = Field(
|
|
487
|
+
default=None,
|
|
488
|
+
description=(
|
|
489
|
+
"Regex that must NOT match. Commands matching both command_pattern and exclude_pattern are skipped."
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
@model_validator(mode="after")
|
|
494
|
+
def _validate_count_bounds(self) -> CommandExecutedCriterion:
|
|
495
|
+
"""Reject impossible ranges where ``max_count < min_count``."""
|
|
496
|
+
if self.max_count is not None and self.max_count < self.min_count:
|
|
497
|
+
raise ValueError(f"max_count ({self.max_count}) must be >= min_count ({self.min_count})")
|
|
498
|
+
return self
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class UiPathEvalCriterion(BaseSuccessCriterion):
|
|
502
|
+
"""Check evaluation results against UiPath agent performance.
|
|
503
|
+
|
|
504
|
+
Pure data model - checking logic in UiPathEvalChecker._check_impl()
|
|
505
|
+
|
|
506
|
+
Threshold semantics: all thresholds are *minimum acceptable values* — a metric
|
|
507
|
+
passes if ``metric_value >= threshold``. For metrics where lower is better
|
|
508
|
+
(e.g. latency), negate or invert the metric on the evaluation side rather than
|
|
509
|
+
here (e.g. use ``throughput = 1 / avg_time`` and threshold on that instead).
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
type: Literal["uipath_eval"] = "uipath_eval"
|
|
513
|
+
agent_name: str = Field(description="Name of the UiPath agent to evaluate")
|
|
514
|
+
eval_set: str = Field(description="Evaluation set identifier")
|
|
515
|
+
thresholds: dict[str, float] = Field(
|
|
516
|
+
description=(
|
|
517
|
+
"Minimum acceptable value per metric (e.g., {'accuracy': 0.8, 'f1': 0.75}). "
|
|
518
|
+
"A metric passes if its value >= the threshold."
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class ClassificationMatchCriterion(BaseSuccessCriterion):
|
|
524
|
+
"""Match a single label written by the agent to a file against ground truth.
|
|
525
|
+
|
|
526
|
+
Reads the file, normalizes the content (strip + optional lowercase), and
|
|
527
|
+
compares it to ``expected_label``. The observed label is the canonical form
|
|
528
|
+
from ``allowed_labels`` when it matches; otherwise ``'(none)'`` when the
|
|
529
|
+
file is missing / empty and ``'(other)'`` when the content is not in the
|
|
530
|
+
allowed set. Both are recorded as sentinels so the suite rollup can show
|
|
531
|
+
them as real failure classes in the confusion matrix.
|
|
532
|
+
|
|
533
|
+
The checker returns a ``ClassificationCriterionResult`` (subclass of
|
|
534
|
+
``CriterionResult``) carrying observed + expected labels, which the
|
|
535
|
+
suite aggregator reads to compute P/R/F1 per class and a confusion matrix.
|
|
536
|
+
|
|
537
|
+
Example YAML:
|
|
538
|
+
|
|
539
|
+
success_criteria:
|
|
540
|
+
- type: "classification_match"
|
|
541
|
+
path: "result.txt"
|
|
542
|
+
expected_label: "positive"
|
|
543
|
+
allowed_labels: [positive, negative]
|
|
544
|
+
description: "Sentiment label matches ground truth"
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
type: Literal["classification_match"] = "classification_match"
|
|
548
|
+
path: str = Field(description="Path to the file (relative to sandbox) containing the agent's predicted label")
|
|
549
|
+
expected_label: str = Field(description="Ground-truth label for this row")
|
|
550
|
+
allowed_labels: list[str] = Field(
|
|
551
|
+
min_length=1,
|
|
552
|
+
description="Canonical label set. File content not in this set is treated as '(other)'.",
|
|
553
|
+
)
|
|
554
|
+
case_sensitive: bool = Field(
|
|
555
|
+
default=False,
|
|
556
|
+
description="When False (default), matching is case-insensitive and labels are canonicalised.",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class SkillTriggeredCriterion(BaseSuccessCriterion):
|
|
561
|
+
"""Binary classifier: did the agent engage the target skill during the run?
|
|
562
|
+
|
|
563
|
+
Agent-agnostic. Observed label is ``"yes"`` when ``turn_records`` show the
|
|
564
|
+
skill engaged under ``skill_name`` — Claude via an explicit ``Skill`` tool
|
|
565
|
+
call, or any agent without that tool (e.g. Codex) by reading the skill's
|
|
566
|
+
files off disk (a command parameter contains ``skills/<skill_name>/``).
|
|
567
|
+
Otherwise ``"no"``. Expected label is ``"yes"`` iff ``expected_skill == skill_name``.
|
|
568
|
+
|
|
569
|
+
Stack one criterion per skill against a single dataset labeled with
|
|
570
|
+
``expected_skill`` (the row's true skill, ``""`` for negatives) to get
|
|
571
|
+
per-skill confusion matrices from the same agent traces.
|
|
572
|
+
|
|
573
|
+
Returns a ``ClassificationCriterionResult`` so the suite-level
|
|
574
|
+
aggregator produces accuracy / recall / F1 / confusion.
|
|
575
|
+
|
|
576
|
+
Example YAML:
|
|
577
|
+
|
|
578
|
+
success_criteria:
|
|
579
|
+
- type: "skill_triggered"
|
|
580
|
+
description: "uipath-maestro-flow activation"
|
|
581
|
+
skill_name: uipath-maestro-flow
|
|
582
|
+
expected_skill: "${row.expected_skill}"
|
|
583
|
+
suite_thresholds: {recall.yes: 0.70}
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
requires_agent: ClassVar[bool] = True
|
|
587
|
+
|
|
588
|
+
type: Literal["skill_triggered"] = "skill_triggered"
|
|
589
|
+
expected_skill: str = Field(
|
|
590
|
+
description="The row's expected skill (after substitution); empty string '' for negatives.",
|
|
591
|
+
)
|
|
592
|
+
skill_name: str = Field(
|
|
593
|
+
description="Only count Skill invocations whose 'skill' parameter matches this name.",
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class LLMJudgeCriterion(BaseSuccessCriterion):
|
|
598
|
+
"""Have an LLM grade the task's final state against an author-supplied prompt.
|
|
599
|
+
|
|
600
|
+
The judge can be given any combination of: sandbox files, the agent's last-turn
|
|
601
|
+
output, a tool-call summary, and the reference solution. It reports its verdict
|
|
602
|
+
by emitting a single ``submit_verdict`` tool call with ``score`` (float),
|
|
603
|
+
``rationale`` (string), and ``findings`` (list of strings). Tool-call shape is
|
|
604
|
+
forced via ``tool_choice`` on every backend; the model never returns prose.
|
|
605
|
+
|
|
606
|
+
Continuous scoring. LLM error or a "did not call submit_verdict" diagnostic
|
|
607
|
+
-> 0.0 with error. Score is clamped to [0.0, 1.0] by the ``JudgeVerdict``
|
|
608
|
+
validator. Non-numeric / non-finite score -> 0.0 with error.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
# Strict YAML-key validation: catch typos at load time rather than silently
|
|
612
|
+
# ignoring an unknown key (e.g. ``capture_transcripts:`` instead of
|
|
613
|
+
# ``capture_transcript:``) and producing a misconfigured judge.
|
|
614
|
+
model_config = ConfigDict(extra="forbid")
|
|
615
|
+
|
|
616
|
+
type: Literal["llm_judge"] = "llm_judge"
|
|
617
|
+
|
|
618
|
+
# Override the BaseSuccessCriterion default of 0.9 — that's calibrated for binary
|
|
619
|
+
# checks like file_exists where partial = fail. Judges produce continuous scores
|
|
620
|
+
# that rarely emit 1.0 even for excellent solutions; 0.7 matches the "good enough
|
|
621
|
+
# for a strict reviewer" semantics most authors want. Override per task as needed.
|
|
622
|
+
pass_threshold: float = Field(default=0.7, ge=0.0, le=1.0, description="Minimum score to pass (default 0.7).")
|
|
623
|
+
|
|
624
|
+
enabled: bool = Field(
|
|
625
|
+
default=True,
|
|
626
|
+
description=(
|
|
627
|
+
"Master toggle for this criterion. When False the judge is NOT called — the criterion "
|
|
628
|
+
"returns a skipped result (score=1.0, details='(skipped: enabled=false)') with no LLM "
|
|
629
|
+
"cost. Useful for A/B comparisons across experiment variants where you want to keep the "
|
|
630
|
+
"criterion in the YAML but not run it under a specific variant."
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
prompt: str = Field(
|
|
634
|
+
description=(
|
|
635
|
+
"Grading instructions shown to the judge. Describe what 'good' looks like "
|
|
636
|
+
"and how observations map to a 0.0-1.0 score."
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
files: list[str] = Field(
|
|
640
|
+
default_factory=list,
|
|
641
|
+
description=(
|
|
642
|
+
"Paths whose contents are shown to the judge. Plain entries are sandbox-relative; "
|
|
643
|
+
"entries prefixed with '$TASK_DIR/' are read from the host filesystem relative to "
|
|
644
|
+
"the task YAML's parent directory (useful for shared rubrics outside the sandbox). "
|
|
645
|
+
"Missing files are rendered as '<file not found>' so the rubric can penalize them."
|
|
646
|
+
),
|
|
647
|
+
)
|
|
648
|
+
include_reference: bool = Field(
|
|
649
|
+
default=True,
|
|
650
|
+
description=(
|
|
651
|
+
"When true (default) and task.reference is set, include the reference solution in "
|
|
652
|
+
"the judge prompt. Silently omitted if no reference is configured. Never shown to "
|
|
653
|
+
"the agent. Set to false if you want the reference to drive a non-judge consumer "
|
|
654
|
+
"(e.g. ``reference_comparison``) without showing it to the LLM grader."
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
include_agent_output: bool = Field(
|
|
658
|
+
default=False,
|
|
659
|
+
description=(
|
|
660
|
+
"When true, include the latest agent turn's raw output in the judge prompt. "
|
|
661
|
+
"Wrapped as UNTRUSTED DATA. No-op when turn_records is unavailable. Default false "
|
|
662
|
+
"because the agent's narration is usually redundant with the files it produced "
|
|
663
|
+
"(declared via ``files`` or visible to the agent_judge via tool access)."
|
|
664
|
+
),
|
|
665
|
+
)
|
|
666
|
+
include_tool_calls: bool = Field(
|
|
667
|
+
default=False,
|
|
668
|
+
description=(
|
|
669
|
+
"When true, include a summary of the latest agent turn's tool calls "
|
|
670
|
+
"(via summarize_commands). No-op when turn_records is unavailable."
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
include_dialog: bool = Field(
|
|
674
|
+
default=False,
|
|
675
|
+
description=(
|
|
676
|
+
"When true, include the full user<->agent conversation across all turns "
|
|
677
|
+
"in the judge prompt. In simulation mode the user side is generated by an "
|
|
678
|
+
"LLM simulator and may invent premises — the judge should treat any claim "
|
|
679
|
+
"made only by the simulated user as possibly fabricated, and not penalize "
|
|
680
|
+
"the agent for going along with it unless the task description contradicts it."
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
max_dialog_chars: int = Field(
|
|
684
|
+
default=80_000,
|
|
685
|
+
gt=0,
|
|
686
|
+
description=(
|
|
687
|
+
"Aggregate cap on dialog text rendered into the judge prompt. Prevents an "
|
|
688
|
+
"N-turn simulation from blowing out the judge's context window. Per-message "
|
|
689
|
+
"truncation uses max_file_chars; trailing turns are dropped when this "
|
|
690
|
+
"aggregate budget is exceeded (a degraded note is recorded)."
|
|
691
|
+
),
|
|
692
|
+
)
|
|
693
|
+
model: str = Field(
|
|
694
|
+
default=DEFAULT_JUDGE_MODEL,
|
|
695
|
+
description=(
|
|
696
|
+
"Judge model id (e.g. 'anthropic.claude-sonnet-4-6'). "
|
|
697
|
+
"On a BedrockRoute / DirectRoute the value is auto-translated: "
|
|
698
|
+
"trailing '-vN[:M]' suffixes and the 'anthropic.' prefix are stripped where "
|
|
699
|
+
"the backend doesn't accept them; on Bedrock the cross-region inference-profile "
|
|
700
|
+
"prefix is added based on AWS_REGION."
|
|
701
|
+
),
|
|
702
|
+
)
|
|
703
|
+
temperature: float = Field(default=0.0, ge=0.0, le=2.0)
|
|
704
|
+
max_tokens: int = Field(
|
|
705
|
+
default=2000,
|
|
706
|
+
gt=0,
|
|
707
|
+
description=(
|
|
708
|
+
"Output token cap. Defaults to 2000 — large enough for the verbose verdict "
|
|
709
|
+
"(score + rationale + a handful of findings) without runaway."
|
|
710
|
+
),
|
|
711
|
+
)
|
|
712
|
+
max_file_chars: int = Field(
|
|
713
|
+
default=20_000,
|
|
714
|
+
gt=0,
|
|
715
|
+
description="Per-file content truncation applied before building the prompt.",
|
|
716
|
+
)
|
|
717
|
+
capture_transcript: bool = Field(
|
|
718
|
+
default=True,
|
|
719
|
+
description=(
|
|
720
|
+
"When true, persist a ``JudgeTranscript`` (raw verdict + rendered prompts + "
|
|
721
|
+
"token usage) to a sibling ``judge-<idx>.yaml`` file next to ``task.json``. "
|
|
722
|
+
"Set to false to drop the transcript when on-disk size matters (e.g. 1000-row "
|
|
723
|
+
"datasets). The verbose ``findings`` field on the result is persisted regardless — "
|
|
724
|
+
"only the per-call transcript file is gated by this flag."
|
|
725
|
+
),
|
|
726
|
+
)
|
|
727
|
+
max_transcript_chars: int = Field(
|
|
728
|
+
default=100_000,
|
|
729
|
+
gt=0,
|
|
730
|
+
description=(
|
|
731
|
+
"Aggregate cap on captured transcript text (raw_verdict + judge_prompt + "
|
|
732
|
+
"judge_system_prompt, plus tool-call detail / result preview lines for "
|
|
733
|
+
"agent_judge). Budget is split 60% verdict / 30% prompt / 10% system. "
|
|
734
|
+
"Truncation marks the transcript as ``truncated=True``."
|
|
735
|
+
),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
@model_validator(mode="before")
|
|
739
|
+
@classmethod
|
|
740
|
+
def _reject_verdict_channel(cls, data: Any) -> Any:
|
|
741
|
+
return _reject_removed_verdict_channel(data)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
class AgentJudgeCriterion(BaseSuccessCriterion):
|
|
745
|
+
"""Spawn a Claude Code SDK agent as the judge.
|
|
746
|
+
|
|
747
|
+
The judge runs in an isolated copy of the sandbox with tool access (Bash, Read,
|
|
748
|
+
Glob, Grep by default) and reports its verdict by calling the in-process
|
|
749
|
+
``submit_verdict`` MCP tool (``mcp__coder_eval_judge__submit_verdict``,
|
|
750
|
+
force-added to ``allowed_tools``) exactly once with ``score`` / ``rationale``
|
|
751
|
+
/ ``findings``. The SDK has no ``tool_choice`` equivalent so the channel
|
|
752
|
+
relies on system-prompt discipline; a judge that never calls the tool
|
|
753
|
+
scores 0.0 with a "did not call submit_verdict" diagnostic.
|
|
754
|
+
|
|
755
|
+
File access: the judge has live access to the sandbox copy as its working
|
|
756
|
+
directory and can load files via its tools (``Read``, ``Glob``). The optional
|
|
757
|
+
``files`` field pre-attaches selected file contents to the prompt envelope —
|
|
758
|
+
useful when you want a fast verdict (single turn, no tool calls) or want to
|
|
759
|
+
grade with the narrowest tool surface (``allowed_tools=[]``). Pre-attach and
|
|
760
|
+
tool-driven inspection compose: the judge sees the pre-attached blocks and
|
|
761
|
+
can still ``Read`` anything else.
|
|
762
|
+
|
|
763
|
+
SECURITY: The judge runs with the evaluator's API credentials and can execute
|
|
764
|
+
arbitrary Bash by default. Four attack surfaces:
|
|
765
|
+
1. Malicious generation artifacts the judge executes (e.g. via `python x.py`).
|
|
766
|
+
2. Prompt injection from included agent_output / tool-call summaries.
|
|
767
|
+
3. Credential exfiltration via any network-capable tool (primarily Bash).
|
|
768
|
+
4. Hooks / MCP servers planted by the main agent (e.g. `.claude/settings.json`
|
|
769
|
+
or `.mcp.json` dropped into the sandbox) that would run before any LLM turn
|
|
770
|
+
and before allowed_tools gating. The judge sets ``setting_sources=[]`` and
|
|
771
|
+
excludes both paths from the sandbox copy, so neither gets loaded.
|
|
772
|
+
Use ``llm_judge`` for scenarios with adversarial generation. Narrow
|
|
773
|
+
``allowed_tools`` per task when Bash is not needed.
|
|
774
|
+
|
|
775
|
+
Continuous scoring. Verdict-validation errors or a "did not call" diagnostic
|
|
776
|
+
-> 0.0. Score clamped to [0.0, 1.0] by the ``JudgeVerdict`` validator.
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
# Strict YAML-key validation: catch typos at load time rather than silently
|
|
780
|
+
# ignoring an unknown key and producing a misconfigured judge.
|
|
781
|
+
model_config = ConfigDict(extra="forbid")
|
|
782
|
+
|
|
783
|
+
type: Literal["agent_judge"] = "agent_judge"
|
|
784
|
+
|
|
785
|
+
# Override the BaseSuccessCriterion default of 0.9 — that's calibrated for binary
|
|
786
|
+
# checks. Judges produce continuous scores that rarely emit 1.0; 0.7 matches the
|
|
787
|
+
# "good enough for a strict reviewer" semantics most authors want. Override per task.
|
|
788
|
+
pass_threshold: float = Field(default=0.7, ge=0.0, le=1.0, description="Minimum score to pass (default 0.7).")
|
|
789
|
+
|
|
790
|
+
enabled: bool = Field(
|
|
791
|
+
default=True,
|
|
792
|
+
description=(
|
|
793
|
+
"Master toggle for this criterion. When False the judge is NOT spawned — the criterion "
|
|
794
|
+
"returns a skipped result (score=1.0, details='(skipped: enabled=false)') with no LLM "
|
|
795
|
+
"cost. Useful for A/B comparisons across experiment variants where you want to keep the "
|
|
796
|
+
"criterion in the YAML but not run it under a specific variant."
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
# Prompt & context — mirrors LLMJudgeCriterion for author consistency
|
|
800
|
+
prompt: str = Field(description="Evaluation instructions for the judge agent")
|
|
801
|
+
files: list[str] = Field(
|
|
802
|
+
default_factory=list,
|
|
803
|
+
description=(
|
|
804
|
+
"Paths whose contents are pre-attached to the judge prompt. Plain entries are "
|
|
805
|
+
"sandbox-relative; entries prefixed with '$TASK_DIR/' are read from the host "
|
|
806
|
+
"filesystem relative to the task YAML's parent directory (useful for shared rubrics "
|
|
807
|
+
"outside the sandbox). Missing files are rendered as '<file not found>' so the "
|
|
808
|
+
"rubric can penalize them. Empty by default — without entries, the judge inspects "
|
|
809
|
+
"the sandbox copy via its tools instead."
|
|
810
|
+
),
|
|
811
|
+
)
|
|
812
|
+
include_reference: bool = Field(
|
|
813
|
+
default=True,
|
|
814
|
+
description=(
|
|
815
|
+
"When true (default) and task.reference is set, mount the reference for the judge. "
|
|
816
|
+
"For ``code`` / ``file`` references, the content is inlined into the prompt. For "
|
|
817
|
+
"``directory`` references, the tree is copied into ``_reference/`` in the judge's "
|
|
818
|
+
"working dir for Read/Glob browsing. Silently omitted if no reference is configured. "
|
|
819
|
+
"Set to false if a reference is configured for ``reference_comparison`` only and "
|
|
820
|
+
"should NOT be visible to the LLM grader."
|
|
821
|
+
),
|
|
822
|
+
)
|
|
823
|
+
include_agent_output: bool = Field(
|
|
824
|
+
default=False,
|
|
825
|
+
description=(
|
|
826
|
+
"Include the latest agent turn's raw output in the judge prompt (UNTRUSTED). "
|
|
827
|
+
"Default false because agent_judge has live tool access to the sandbox copy and "
|
|
828
|
+
"can ``Read`` the agent's files directly — inlining narration is usually redundant."
|
|
829
|
+
),
|
|
830
|
+
)
|
|
831
|
+
include_tool_calls: bool = Field(
|
|
832
|
+
default=False,
|
|
833
|
+
description="Include summarized tool-call telemetry from the latest agent turn.",
|
|
834
|
+
)
|
|
835
|
+
include_dialog: bool = Field(
|
|
836
|
+
default=False,
|
|
837
|
+
description=(
|
|
838
|
+
"Include the full user<->agent conversation across all turns. In simulation "
|
|
839
|
+
"mode the user side is generated by an LLM simulator and may invent premises — "
|
|
840
|
+
"the judge should treat any claim made only by the simulated user as possibly "
|
|
841
|
+
"fabricated, and not penalize the agent for going along with it unless the task "
|
|
842
|
+
"description contradicts it."
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
max_dialog_chars: int = Field(
|
|
846
|
+
default=80_000,
|
|
847
|
+
gt=0,
|
|
848
|
+
description=(
|
|
849
|
+
"Aggregate cap on dialog text rendered into the judge prompt. Prevents an "
|
|
850
|
+
"N-turn simulation from blowing out the judge's context window. Per-message "
|
|
851
|
+
"truncation uses max_file_chars; trailing turns are dropped when this "
|
|
852
|
+
"aggregate budget is exceeded (a degraded note is recorded)."
|
|
853
|
+
),
|
|
854
|
+
)
|
|
855
|
+
max_file_chars: int = Field(
|
|
856
|
+
default=20_000,
|
|
857
|
+
gt=0,
|
|
858
|
+
description=(
|
|
859
|
+
"Per-message truncation budget for trajectory blocks (agent_output, dialog turns). "
|
|
860
|
+
"agent_judge no longer pre-attaches files — the judge reads them via its tools — so "
|
|
861
|
+
"this only applies to trajectory injection."
|
|
862
|
+
),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Judge agent config
|
|
866
|
+
max_turns: int = Field(
|
|
867
|
+
default=50,
|
|
868
|
+
gt=0,
|
|
869
|
+
description=(
|
|
870
|
+
"Inner-loop turn limit for the judge agent. Generous default — typical judge runs "
|
|
871
|
+
"use 5-15 turns; the cap mostly matters when grading complex multi-file solutions "
|
|
872
|
+
"where the judge needs to read across many files. ``turn_timeout`` (default 300s) "
|
|
873
|
+
"bounds wall-clock per turn, so the practical cost cap is tokens, not time."
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
turn_timeout: int = Field(
|
|
877
|
+
default=300,
|
|
878
|
+
ge=10,
|
|
879
|
+
description="Wall-clock timeout for the judge turn (seconds). Minimum 10.",
|
|
880
|
+
)
|
|
881
|
+
# Intentionally the closed built-in AgentConfig union, NOT ResolvedAgentConfig:
|
|
882
|
+
# agent_judge spawns a Claude Code SDK sub-agent (SubAgentRunner) with evaluator
|
|
883
|
+
# credentials, so a plugin agent kind is deliberately not accepted as a judge.
|
|
884
|
+
agent: AgentConfig = Field(
|
|
885
|
+
default_factory=_default_judge_agent_config,
|
|
886
|
+
description=(
|
|
887
|
+
"Judge agent configuration (built-in kinds only — agent_judge runs a Claude Code "
|
|
888
|
+
"sub-agent). Defaults to a sonnet, bypass-permissions, read-only toolkit suitable "
|
|
889
|
+
"for investigation-style judging. Override fields per task as needed. For security, "
|
|
890
|
+
"the judge always runs with setting_sources=[] regardless of what this field "
|
|
891
|
+
"declares — see _build_agent_config."
|
|
892
|
+
),
|
|
893
|
+
)
|
|
894
|
+
capture_transcript: bool = Field(
|
|
895
|
+
default=True,
|
|
896
|
+
description=(
|
|
897
|
+
"When true, persist a ``JudgeTranscript`` (tool calls + token usage + raw verdict + "
|
|
898
|
+
"rendered prompts) to a sibling ``judge-<idx>.yaml`` file next to ``task.json``. "
|
|
899
|
+
"Set to false to drop the transcript when on-disk size matters (e.g. 1000-row "
|
|
900
|
+
"datasets). The verbose ``findings`` field on the result is persisted regardless — "
|
|
901
|
+
"only the trajectory log is gated by this flag."
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
max_transcript_chars: int = Field(
|
|
905
|
+
default=100_000,
|
|
906
|
+
gt=0,
|
|
907
|
+
description=(
|
|
908
|
+
"Aggregate cap on captured transcript text (raw_verdict + judge_prompt + "
|
|
909
|
+
"judge_system_prompt + tool detail / result_preview lines). Budget is split "
|
|
910
|
+
"60% verdict / 30% prompt / 10% system, with tool calls taking priority. "
|
|
911
|
+
"Truncation marks the transcript as ``truncated=True``."
|
|
912
|
+
),
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
@model_validator(mode="before")
|
|
916
|
+
@classmethod
|
|
917
|
+
def _reject_verdict_channel(cls, data: Any) -> Any:
|
|
918
|
+
return _reject_removed_verdict_channel(data)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
# Discriminated union of all success criteria. The `type` tag is REQUIRED in
|
|
922
|
+
# dict/YAML input: a missing or unknown tag raises one crisp discriminator error
|
|
923
|
+
# instead of smart-union coercion across every variant. The per-variant
|
|
924
|
+
# `type: Literal[...] = "<tag>"` defaults remain, so direct construction
|
|
925
|
+
# (e.g. FileExistsCriterion(path=...)) and model_dump() serialization are unaffected.
|
|
926
|
+
SuccessCriterion = Annotated[
|
|
927
|
+
FileExistsCriterion
|
|
928
|
+
| FileContainsCriterion
|
|
929
|
+
| RunCommandCriterion
|
|
930
|
+
| FileMatchesRegexCriterion
|
|
931
|
+
| FileCheckCriterion
|
|
932
|
+
| JsonCheckCriterion
|
|
933
|
+
| ReferenceComparisonCriterion
|
|
934
|
+
| CommandExecutedCriterion
|
|
935
|
+
| CommandsEfficiencyCriterion
|
|
936
|
+
| UiPathEvalCriterion
|
|
937
|
+
| ClassificationMatchCriterion
|
|
938
|
+
| SkillTriggeredCriterion
|
|
939
|
+
| LLMJudgeCriterion
|
|
940
|
+
| AgentJudgeCriterion,
|
|
941
|
+
Field(discriminator="type"),
|
|
942
|
+
]
|