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,121 @@
1
+ """Enumeration types for coder_eval."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Literal
5
+
6
+
7
+ class FinalStatus(StrEnum):
8
+ """Final evaluation status for a task."""
9
+
10
+ SUCCESS = "SUCCESS"
11
+ FAILURE = "FAILURE"
12
+ ERROR = "ERROR"
13
+ BUILD_FAILED = "BUILD_FAILED"
14
+ TIMEOUT = "TIMEOUT"
15
+ MAX_TURNS_EXHAUSTED = "MAX_TURNS_EXHAUSTED"
16
+ TOKEN_BUDGET_EXCEEDED = "TOKEN_BUDGET_EXCEEDED"
17
+ COST_BUDGET_EXCEEDED = "COST_BUDGET_EXCEEDED"
18
+
19
+ @property
20
+ def category(self) -> Literal["succeeded", "failed", "error"]:
21
+ """Classify this status into a reporting category (the SSOT for failed/succeeded/error)."""
22
+ return _STATUS_CATEGORIES[self]
23
+
24
+ @property
25
+ def icon(self) -> str:
26
+ """Single-character icon for reports and CLI output."""
27
+ return _STATUS_ICONS[self]
28
+
29
+
30
+ # Every FinalStatus maps to exactly one reporting category, listed EXPLICITLY (no
31
+ # catch-all default) so a newly-added status fails the assert below until it is
32
+ # classified — rather than silently collapsing into "failed" (which would skew
33
+ # reports AND the telemetry Category dimension). Mirrors the _STATUS_ICONS guard.
34
+ _STATUS_CATEGORIES: dict[FinalStatus, Literal["succeeded", "failed", "error"]] = {
35
+ FinalStatus.SUCCESS: "succeeded",
36
+ FinalStatus.FAILURE: "failed",
37
+ FinalStatus.ERROR: "error",
38
+ # A failed image build is an environment/setup error, not a task outcome —
39
+ # group it with ERROR so reports/telemetry don't read it as a legitimate
40
+ # task failure the agent could have avoided.
41
+ FinalStatus.BUILD_FAILED: "error",
42
+ FinalStatus.TIMEOUT: "failed",
43
+ FinalStatus.MAX_TURNS_EXHAUSTED: "failed",
44
+ FinalStatus.TOKEN_BUDGET_EXCEEDED: "failed",
45
+ FinalStatus.COST_BUDGET_EXCEEDED: "failed",
46
+ }
47
+
48
+ assert set(_STATUS_CATEGORIES) == set(FinalStatus), "Missing category for FinalStatus member"
49
+
50
+
51
+ _STATUS_ICONS: dict[FinalStatus, str] = {
52
+ FinalStatus.SUCCESS: "+",
53
+ FinalStatus.FAILURE: "-",
54
+ FinalStatus.ERROR: "!",
55
+ FinalStatus.BUILD_FAILED: "B",
56
+ FinalStatus.TIMEOUT: "T",
57
+ FinalStatus.MAX_TURNS_EXHAUSTED: "M",
58
+ FinalStatus.TOKEN_BUDGET_EXCEEDED: "#",
59
+ FinalStatus.COST_BUDGET_EXCEEDED: "$",
60
+ }
61
+
62
+ assert set(_STATUS_ICONS) == set(FinalStatus), "Missing icon for FinalStatus member"
63
+
64
+
65
+ class ApiBackend(StrEnum):
66
+ """API backend for LLM calls."""
67
+
68
+ DIRECT = "direct" # Anthropic API directly (ANTHROPIC_API_KEY)
69
+ BEDROCK = "bedrock" # AWS Bedrock (bearer token auth)
70
+
71
+
72
+ class PermissionMode(StrEnum):
73
+ """Permission modes for agent tool access."""
74
+
75
+ DEFAULT = "default"
76
+ ACCEPT_EDITS = "acceptEdits"
77
+ PLAN = "plan"
78
+ BYPASS_PERMISSIONS = "bypassPermissions"
79
+
80
+
81
+ class PreservationMode(StrEnum):
82
+ """How a task's sandbox is persisted (or not) after execution.
83
+
84
+ The run-level CLI default is *driver-derived* (resolved at the dispatch
85
+ seam in ``orchestration.batch``): ``docker`` → ``DIRECT_WRITE`` (the
86
+ container is isolated, and writing straight to the bind-mounted artifacts
87
+ dir avoids a cross-mount copy), every other driver → ``MOVE_ON_WRITE``
88
+ (running under ``run_dir/artifacts`` on a shared host would let parent-dir
89
+ ``node_modules`` contaminate Node tool resolution — see MST-9795/PR #257).
90
+ An explicit ``--preservation-mode`` always wins over that default.
91
+ """
92
+
93
+ NONE = "NONE" # Run in a tempdir, delete it on cleanup (no artifacts kept).
94
+ MOVE_ON_WRITE = "MOVE_ON_WRITE" # Run in a tempdir, shutil.move into run_dir/artifacts at the end.
95
+ DIRECT_WRITE = "DIRECT_WRITE" # Run directly in run_dir/artifacts; nothing to move (left in place).
96
+
97
+
98
+ class AgentKind(StrEnum):
99
+ """Named constants for the built-in agent kinds.
100
+
101
+ These are NOT the closed set of valid agent types: ``agent.type`` is an open
102
+ string validated against the ``AgentRegistry`` (which plugins extend via the
103
+ ``coder_eval.plugins`` entry point). This enum just gives the framework's own
104
+ code stable references to the built-ins; the registry is authoritative.
105
+ """
106
+
107
+ CLAUDE_CODE = "claude-code"
108
+ CODEX = "codex"
109
+ ANTIGRAVITY = "antigravity"
110
+ NONE = "none" # Agentless / system task — no coding agent runs; success criteria do all the work.
111
+ UNKNOWN = "unknown" # Used when agent type cannot be determined (e.g., task loading failure)
112
+
113
+
114
+ class AgentState(StrEnum):
115
+ """Possible states of the agent during execution."""
116
+
117
+ WORKING = "working"
118
+ WAITING_FOR_USER = "waiting_for_user"
119
+ CODE_PROPOSAL = "code_proposal"
120
+ FINISHED = "finished"
121
+ ERROR = "error"
@@ -0,0 +1,314 @@
1
+ """Experiment definition models for multi-run configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any, Literal, Self
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
10
+
11
+ from coder_eval.models.enums import FinalStatus
12
+ from coder_eval.models.limits import RunLimits
13
+ from coder_eval.models.mutations import PromptMutation
14
+ from coder_eval.models.results import ConfigLineageEntry, EvaluationResult
15
+ from coder_eval.models.sandbox import SandboxConfig
16
+ from coder_eval.models.tasks import PostRunCommand, PreRunCommand, TaskDefinition
17
+ from coder_eval.models.templates import TemplateSource
18
+
19
+
20
+ class ExperimentVariant(BaseModel):
21
+ """A named configuration variant within an experiment."""
22
+
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ variant_id: str = Field(description="Unique identifier for this variant (e.g., 'sonnet', 'opus')")
26
+ description: str = Field(default="", description="Human-readable description of what this variant tests")
27
+ agent: dict[str, Any] | None = Field(default=None, description="Partial agent config overrides")
28
+ simulation: dict[str, Any] | None = Field(
29
+ default=None,
30
+ description=(
31
+ "Partial SimulationConfig overrides for this variant. Shallow-merged onto the task's "
32
+ "simulation block (and/or experiment defaults) — common use cases are overriding the "
33
+ "simulator persona/model/temperature per variant."
34
+ ),
35
+ )
36
+ repeats: int | None = Field(
37
+ default=None,
38
+ ge=1,
39
+ description="Number of replicates for each task under this variant. None = inherit.",
40
+ )
41
+ template_sources: list[TemplateSource] | None = Field(
42
+ default=None, description="Additional template sources appended after task's base templates"
43
+ )
44
+ prompt_mutations: list[PromptMutation] | None = Field(
45
+ default=None, description="Ordered list of mutations to apply to the task's initial_prompt"
46
+ )
47
+ initial_prompt: str | None = Field(
48
+ default=None,
49
+ description="Full replacement for the task's initial_prompt. Mutually exclusive with prompt_mutations.",
50
+ )
51
+ initial_prompt_file: str | None = Field(
52
+ default=None,
53
+ description=(
54
+ "Path to a file containing a full replacement initial_prompt (relative to experiment YAML). "
55
+ "Mutually exclusive with initial_prompt and prompt_mutations."
56
+ ),
57
+ )
58
+ run_limits: RunLimits | None = Field(
59
+ default=None,
60
+ description=(
61
+ "Per-variant overrides for the task's run_limits block. Field-merge — "
62
+ "per-key precedence inside the block; keys absent here leave the task-level "
63
+ "value intact."
64
+ ),
65
+ )
66
+ driver: Literal["tempdir", "docker"] | None = Field(
67
+ default=None,
68
+ description=(
69
+ "Override the sandbox driver for this variant. Slots into layer 4 of the "
70
+ "5-layer config merge; None inherits from task/experiment-defaults. "
71
+ "Enables variant-A=tempdir vs variant-B=docker comparisons."
72
+ ),
73
+ )
74
+
75
+ @model_validator(mode="after")
76
+ def check_prompt_exclusivity(self) -> Self:
77
+ """Ensure prompt_mutations, initial_prompt, and initial_prompt_file are mutually exclusive."""
78
+ set_fields = []
79
+ if self.prompt_mutations is not None:
80
+ set_fields.append("prompt_mutations")
81
+ if self.initial_prompt is not None:
82
+ set_fields.append("initial_prompt")
83
+ if self.initial_prompt_file is not None:
84
+ set_fields.append("initial_prompt_file")
85
+ if len(set_fields) > 1:
86
+ raise ValueError(f"Only one of {', '.join(set_fields)} can be provided, not multiple")
87
+ return self
88
+
89
+
90
+ class ExperimentDefaults(BaseModel):
91
+ """Default settings applied to all variants (overridable per-variant and per-task)."""
92
+
93
+ model_config = ConfigDict(extra="forbid")
94
+
95
+ repeats: int | None = Field(
96
+ default=None,
97
+ ge=1,
98
+ description="Default number of replicates across all variants. None = 1 (no repetition).",
99
+ )
100
+ agent: dict[str, Any] | None = Field(default=None, description="Partial agent config defaults")
101
+ simulation: dict[str, Any] | None = Field(
102
+ default=None,
103
+ description=(
104
+ "Default simulation config applied to tasks that do not set one explicitly. "
105
+ "Fields map to SimulationConfig (enabled, persona, goal, model, max_turns, n_trials, ...)."
106
+ ),
107
+ )
108
+ template_sources: list[TemplateSource] | None = Field(
109
+ default=None, description="Additional template sources appended after task's base templates (for all variants)"
110
+ )
111
+ prompt_mutations: list[PromptMutation] | None = Field(
112
+ default=None, description="Default prompt mutations applied to all variants (before variant-specific mutations)"
113
+ )
114
+ post_run: list[PostRunCommand] | None = Field(
115
+ default=None,
116
+ description="Default post-run commands appended after each task's own post_run (run for every task).",
117
+ )
118
+ pre_run: list[PreRunCommand] | None = Field(
119
+ default=None,
120
+ description="Default pre-run commands prepended before each task's own pre_run (run for every task).",
121
+ )
122
+ run_limits: RunLimits | None = Field(
123
+ default=None,
124
+ description=(
125
+ "Default run-time caps (turns, wall-clock, tokens, USD). "
126
+ "Field-merge — task and variant layers override individual keys without replacing the block."
127
+ ),
128
+ )
129
+ driver: Literal["tempdir", "docker"] | None = Field(
130
+ default=None,
131
+ description=(
132
+ "Default sandbox driver applied to all tasks under this experiment. Slots into "
133
+ "layer 2 of the 5-layer merge; variant- and task-level driver fields override it."
134
+ ),
135
+ )
136
+ sandbox: SandboxConfig | None = Field(
137
+ default=None,
138
+ description=(
139
+ "Default sandbox config applied to all tasks under this experiment. "
140
+ "Field-merge — task and variant layers override individual keys without replacing the block."
141
+ ),
142
+ )
143
+
144
+
145
+ class ExperimentDefinition(BaseModel):
146
+ """Complete experiment definition with base settings and variants."""
147
+
148
+ model_config = ConfigDict(extra="forbid")
149
+
150
+ experiment_id: str = Field(description="Kebab-case identifier for this experiment")
151
+ description: str = Field(default="", description="Human-readable description")
152
+ defaults: ExperimentDefaults | None = Field(default=None, description="Default settings applied to all variants")
153
+ variants: list[ExperimentVariant] = Field(description="Configuration variants (at least 1)")
154
+
155
+ @field_validator("experiment_id")
156
+ @classmethod
157
+ def validate_experiment_id(cls, v: str) -> str:
158
+ if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", v):
159
+ raise ValueError(f"experiment_id '{v}' must be kebab-case (e.g., 'model-comparison')")
160
+ return v
161
+
162
+ @model_validator(mode="after")
163
+ def validate_variants(self) -> ExperimentDefinition:
164
+ if len(self.variants) < 1:
165
+ raise ValueError("Experiment must have at least 1 variant")
166
+ ids = [v.variant_id for v in self.variants]
167
+ if len(ids) != len(set(ids)):
168
+ raise ValueError(f"Variant IDs must be unique, got duplicates in: {ids}")
169
+ return self
170
+
171
+
172
+ class VariantResult(BaseModel): # noqa: CE009 -- persisted result model; round-trip leniency like models/results.py
173
+ """Result for a single variant on a single task."""
174
+
175
+ variant_id: str
176
+ task_id: str
177
+ weighted_score: float
178
+ final_status: FinalStatus
179
+ duration_seconds: float
180
+ total_tokens: int | None = None
181
+ iteration_count: int | None = None
182
+ total_assistant_turns: int | None = None
183
+ reference_similarity: float | None = None
184
+ replicate_index: int = Field(
185
+ default=0,
186
+ ge=0,
187
+ description="Replicate index for this per-task result (0 when no replicates).",
188
+ )
189
+ replicate_count: int = Field(
190
+ default=1,
191
+ ge=1,
192
+ description="Number of replicates aggregated into this VariantResult (1 when repeats disabled).",
193
+ )
194
+
195
+
196
+ class VariantAggregate(BaseModel): # noqa: CE009 -- persisted result model; round-trip leniency like models/results.py
197
+ """Aggregated statistics for a single variant across all tasks."""
198
+
199
+ variant_id: str
200
+ tasks_run: int
201
+ tasks_succeeded: int
202
+ tasks_failed: int
203
+ tasks_error: int
204
+ average_score: float
205
+ average_duration: float
206
+ total_tokens: int | None = None
207
+ replicate_count: int = Field(
208
+ default=1,
209
+ ge=1,
210
+ description="Replicate multiplicity (modal value across tasks).",
211
+ )
212
+ # Sub-counters of tasks_failed (NOT part of the task_count invariant).
213
+ tasks_token_budget_exceeded: int = Field(
214
+ default=0,
215
+ ge=0,
216
+ description="Subset of tasks_failed where run_limits token caps tripped.",
217
+ )
218
+ tasks_cost_budget_exceeded: int = Field(
219
+ default=0,
220
+ ge=0,
221
+ description="Subset of tasks_failed where run_limits cost cap tripped.",
222
+ )
223
+
224
+ @model_validator(mode="after")
225
+ def _check_task_count_invariant(self) -> VariantAggregate:
226
+ if self.tasks_succeeded + self.tasks_failed + self.tasks_error != self.tasks_run:
227
+ total = f"{self.tasks_succeeded} + {self.tasks_failed} + {self.tasks_error}"
228
+ raise ValueError(f"Task count invariant violated: {total} != {self.tasks_run}")
229
+ return self
230
+
231
+
232
+ class TaskExperimentSummary(BaseModel): # noqa: CE009 -- persisted result model; round-trip leniency like models/results.py
233
+ """Cross-variant summary for a single task."""
234
+
235
+ task_id: str
236
+ variant_results: list[VariantResult]
237
+ best_variant: str
238
+ is_tie: bool = Field(default=False, description="True when multiple variants share the highest score")
239
+ score_spread: float
240
+ replicate_count: int = Field(
241
+ default=1,
242
+ ge=1,
243
+ description="Replicate count per variant on this task. Drives Replicate Statistics rendering.",
244
+ )
245
+
246
+
247
+ class ExperimentResult(BaseModel): # noqa: CE009 -- persisted result model; round-trip leniency like models/results.py
248
+ """Top-level experiment result with per-task summaries and variant aggregates."""
249
+
250
+ experiment_id: str
251
+ description: str
252
+ variant_ids: list[str]
253
+ task_summaries: list[TaskExperimentSummary]
254
+ variant_aggregates: dict[str, VariantAggregate]
255
+ total_duration_seconds: float
256
+ per_replicate_scores: dict[str, dict[str, list[float]]] = Field(
257
+ default_factory=dict,
258
+ description=(
259
+ "Raw weighted_score per replicate, keyed variant_id → task_id → [scores]. "
260
+ "Always populated by aggregate_results; list length equals the replicate count. "
261
+ "Empty dict only on deserialized results from before this field existed."
262
+ ),
263
+ )
264
+
265
+
266
+ class ResolvedTask(BaseModel): # noqa: CE009 -- programmatic resolution model, not YAML input
267
+ """A fully-resolved task ready for batch execution.
268
+
269
+ Created by the resolution phase after applying all 5 config layers
270
+ (default → experiment defaults → task YAML → variant → CLI).
271
+ Consumed by run_batch() as the sole input type.
272
+ """
273
+
274
+ task: TaskDefinition
275
+ task_file: Path
276
+ run_dir: Path
277
+ variant_id: str
278
+ source_yaml: str = ""
279
+ config_lineage: dict[str, ConfigLineageEntry] = Field(default_factory=dict)
280
+ replicate_index: int = Field(
281
+ default=0,
282
+ ge=0,
283
+ description=(
284
+ "0-based replicate index within the (task, variant) group. Set by resolve_all_tasks. "
285
+ "Always 0 for single-shot tasks and for simulation tasks with n_trials=1. "
286
+ "Simulation tasks with n_trials > 1 are expanded into replicate_index=0..n_trials-1."
287
+ ),
288
+ )
289
+
290
+
291
+ class TaskResult(BaseModel): # noqa: CE009 -- persisted result model; round-trip leniency like models/results.py
292
+ """Result from executing a single resolved task.
293
+
294
+ Replaces the untyped dict[str, Any] with keys {task_id, result, duration}
295
+ that previously flowed between run_batch and aggregate_results.
296
+ """
297
+
298
+ task_id: str
299
+ result: EvaluationResult
300
+ duration: float
301
+ variant_id: str
302
+ suite_id: str | None = Field(
303
+ default=None,
304
+ description="Parent suite id when this task came from dataset fan-out. Signal for suite rollup reporting.",
305
+ )
306
+ row_id: str | None = Field(
307
+ default=None,
308
+ description="Row id within the suite (from Dataset.id_field) when this task came from dataset fan-out.",
309
+ )
310
+ replicate_index: int = Field(
311
+ default=0,
312
+ ge=0,
313
+ description="0-based replicate index from ResolvedTask. Populated by batch.py.",
314
+ )
@@ -0,0 +1,90 @@
1
+ """Typed verdict contract for judge-style criteria (llm_judge, agent_judge)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class JudgeVerdict(BaseModel):
12
+ """Typed contract for a judge's verdict.
13
+
14
+ Used both as the tool-input schema for the ``submit_verdict`` call (across
15
+ SDK MCP and Anthropic-native ``tools``) and as the validation target for the
16
+ extractors in ``coder_eval.evaluation.verdict_tool``.
17
+
18
+ Score is clamped to [0.0, 1.0] by a validator (not a Field constraint) so
19
+ out-of-range values from the model become 0.0/1.0 rather than tool-input
20
+ validation errors. Non-finite values (NaN, +/-Infinity) are rejected
21
+ explicitly because ``max(0.0, min(1.0, nan))`` silently yields 1.0.
22
+ Booleans are rejected because Python treats them as ints.
23
+
24
+ ``findings`` carries the audit trail — the system prompt always asks
25
+ the judge to emit it. The field stays optional on the wire so a model
26
+ that drops it still parses (the parser never falls back to score=0.0
27
+ for missing findings), but the empty-findings case should be rare.
28
+ """
29
+
30
+ model_config = {"extra": "ignore"}
31
+
32
+ score: float = Field(description="Verdict score, clamped to [0.0, 1.0]")
33
+ rationale: str = Field(default="", description="1-2 sentence headline summary")
34
+ findings: list[str] = Field(
35
+ default_factory=list,
36
+ description=(
37
+ "Concrete observations the judge cited from the artifacts (e.g. 'main.py:12 missing return'). "
38
+ "Each entry is a short bullet. Empty when the judge omitted them."
39
+ ),
40
+ )
41
+
42
+ @field_validator("score", mode="before")
43
+ @classmethod
44
+ def _clamp_score(cls, v: Any) -> float:
45
+ if isinstance(v, bool):
46
+ raise ValueError(f"score is not a number: {v!r}")
47
+ try:
48
+ f = float(v)
49
+ except (TypeError, ValueError) as e:
50
+ raise ValueError(f"score is not a number: {v!r}") from e
51
+ if not math.isfinite(f):
52
+ raise ValueError(f"score is not finite: {v!r}")
53
+ return max(0.0, min(1.0, f))
54
+
55
+ @field_validator("rationale", mode="before")
56
+ @classmethod
57
+ def _coerce_rationale(cls, v: Any) -> str:
58
+ if v is None:
59
+ return ""
60
+ if not isinstance(v, str):
61
+ raise ValueError(f"rationale must be a string, got {type(v).__name__}")
62
+ # Collapse internal whitespace (newlines, tabs, runs of spaces) to single
63
+ # spaces. Two reasons:
64
+ # 1. ``format_details`` writes "rationale: <text>" on one line; a multi-line
65
+ # rationale would break that single-line invariant.
66
+ # 2. ``reports_html._extract_rationale`` parses by line and grabs only the
67
+ # first one starting with "rationale: " — multi-line content would be
68
+ # silently truncated in the Judge Verdicts card.
69
+ # The schema asks for "1-2 sentence headline summary"; collapsing whitespace
70
+ # makes that contract explicit.
71
+ collapsed = " ".join(v.split())
72
+ if not collapsed:
73
+ # Whitespace-only / empty input surfaces as a validation error so the
74
+ # judge result records the error string instead of silently emitting
75
+ # a blank ``rationale: `` line in ``format_details``. The
76
+ # ``extract_verdict_from_*`` extractors translate the
77
+ # ``ValidationError`` into a JudgeCriterionResult(score=0.0, error=...)
78
+ # — uniform shape across all three backends.
79
+ raise ValueError("rationale is empty after whitespace collapse")
80
+ return collapsed
81
+
82
+ @field_validator("findings", mode="before")
83
+ @classmethod
84
+ def _coerce_findings(cls, v: Any) -> list[str]:
85
+ # Same leniency: drop anything that doesn't look like a list of strings.
86
+ if v is None:
87
+ return []
88
+ if not isinstance(v, list):
89
+ return []
90
+ return [str(x).strip() for x in v if x is not None and str(x).strip()]
@@ -0,0 +1,11 @@
1
+ """Shared judge default constants.
2
+
3
+ Split out of ``models/tasks.py`` so that both ``models/tasks.py`` and
4
+ ``models/criteria.py`` can import ``DEFAULT_JUDGE_MODEL`` without
5
+ introducing an import cycle (``tasks.py`` already imports from
6
+ ``criteria.py``). Keep this a leaf module — it must NOT import from
7
+ ``criteria.py`` / ``tasks.py``.
8
+ """
9
+
10
+ DEFAULT_JUDGE_MODEL = "anthropic.claude-sonnet-4-6"
11
+ """Default model used by the LLM judge (``LLMJudgeCriterion.model``)."""
@@ -0,0 +1,89 @@
1
+ """Run-time budget limits for task execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class RunLimits(BaseModel):
9
+ """Run-time caps that abort a task when exceeded.
10
+
11
+ Unifies structural caps (max_turns, task_timeout, turn_timeout) and
12
+ budget caps (tokens, USD). Budget caps are checked after each completed
13
+ agent turn and are cumulative across all turns of a single task; they
14
+ apply to the subject agent only — judge and simulator token spend are
15
+ not counted.
16
+
17
+ Any subset of fields is valid; an empty block is legal.
18
+ """
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+ max_turns: int | None = Field(
23
+ default=None,
24
+ gt=0,
25
+ description="Max agent inner-loop turns per iteration. None = SDK default.",
26
+ )
27
+ expected_turns: int | None = Field(
28
+ default=None,
29
+ ge=1,
30
+ description=(
31
+ "Soft target for cumulative visible turns across a task. A 'turn' is one "
32
+ "entry in the Turn timeline: each tool call contributes 1, plus 1 for the "
33
+ "final reply when present. "
34
+ "When the running total exceeds this, the orchestrator logs a one-shot "
35
+ "warning and the report renders a badge — the run is NOT aborted "
36
+ "(use max_turns for a hard cap). None disables the check."
37
+ ),
38
+ )
39
+ task_timeout: int | None = Field(
40
+ default=None,
41
+ ge=30,
42
+ description="Max seconds for the entire evaluation loop (all iterations). None = no limit.",
43
+ )
44
+ turn_timeout: int | None = Field(
45
+ default=None,
46
+ ge=10,
47
+ description="Max seconds per agent communicate() call. None = no limit.",
48
+ )
49
+ max_input_tokens: int | None = Field(
50
+ default=None,
51
+ ge=1,
52
+ description="Max cumulative input (prompt) tokens. None = unlimited.",
53
+ )
54
+ max_output_tokens: int | None = Field(
55
+ default=None,
56
+ ge=1,
57
+ description="Max cumulative output (completion) tokens. None = unlimited.",
58
+ )
59
+ max_total_tokens: int | None = Field(
60
+ default=None,
61
+ ge=1,
62
+ description="Max cumulative input+output tokens. None = unlimited.",
63
+ )
64
+ max_usd: float | None = Field(
65
+ default=None,
66
+ gt=0.0,
67
+ description=(
68
+ "Max cumulative cost in USD. Requires per-turn cost reporting "
69
+ "(SDK-provided cost). Silently skipped if cost "
70
+ "is None for every turn."
71
+ ),
72
+ )
73
+ count_cached_input: bool = Field(
74
+ default=False,
75
+ description=(
76
+ "When True, cache_read_input_tokens count toward input/total "
77
+ "budgets. Default False — cached reads are typically free."
78
+ ),
79
+ )
80
+ count_cache_creation: bool = Field(
81
+ default=False,
82
+ description=(
83
+ "When True, cache_creation_input_tokens count toward input/total "
84
+ "budgets. Needed to budget Codex prompt input: the Codex agent buckets "
85
+ "the fresh (full-price) prompt slice into cache_creation, so with this "
86
+ "False a Codex token budget caps only output. Default False preserves "
87
+ "existing behavior (Claude reports real cache-creation writes here)."
88
+ ),
89
+ )