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.
Files changed (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. 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
+ ]