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,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
|
+
)
|