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,787 @@
|
|
|
1
|
+
"""Evaluation results and execution record models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Discriminator, Field, Tag, model_validator
|
|
9
|
+
|
|
10
|
+
from coder_eval.models.agent_config import ResolvedAgentConfig
|
|
11
|
+
from coder_eval.models.criteria import SuccessCriterion
|
|
12
|
+
from coder_eval.models.enums import FinalStatus
|
|
13
|
+
from coder_eval.models.telemetry import (
|
|
14
|
+
CommandStatistics,
|
|
15
|
+
CommandTelemetry,
|
|
16
|
+
TokenUsage,
|
|
17
|
+
TranscriptMessage,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigLineageEntry(BaseModel):
|
|
22
|
+
"""Records which config layer provided a specific value."""
|
|
23
|
+
|
|
24
|
+
value: Any
|
|
25
|
+
source: Literal[
|
|
26
|
+
"default",
|
|
27
|
+
"task",
|
|
28
|
+
"experiment-defaults",
|
|
29
|
+
"variant",
|
|
30
|
+
"cli",
|
|
31
|
+
"mutation",
|
|
32
|
+
]
|
|
33
|
+
source_detail: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskConfigRecord(BaseModel):
|
|
37
|
+
"""Full task configuration snapshot stored in per-task output."""
|
|
38
|
+
|
|
39
|
+
resolved: dict[str, Any] = Field(description="Full resolved TaskDefinition as dict")
|
|
40
|
+
source_yaml: str = Field(description="Raw YAML text from the task file")
|
|
41
|
+
source_file: str | None = Field(default=None, description="Path to the original task YAML")
|
|
42
|
+
lineage: dict[str, ConfigLineageEntry] = Field(default_factory=dict, description="Dotted-path → source layer")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CriterionResult(BaseModel):
|
|
46
|
+
"""Result of checking a single success criterion.
|
|
47
|
+
|
|
48
|
+
``extra="allow"`` lets subclass-specific fields (e.g. ``observed_label`` from
|
|
49
|
+
``ClassificationCriterionResult``, ``analysis`` / ``transcript`` from
|
|
50
|
+
``JudgeCriterionResult``) round-trip through ``EvaluationResult.model_dump`` →
|
|
51
|
+
``model_validate_json``. Without this, the typed list ``list[CriterionResult]``
|
|
52
|
+
silently strips fields that aren't on the base class when reading task.json
|
|
53
|
+
back — losing the audit data that's the whole point of the verbose verdict.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="allow")
|
|
57
|
+
|
|
58
|
+
result_kind: Literal["basic"] = Field(
|
|
59
|
+
default="basic",
|
|
60
|
+
description=(
|
|
61
|
+
"Discriminator for ``CriterionResultUnion`` so subclasses round-trip through "
|
|
62
|
+
"``model_dump_json`` → ``model_validate_json`` with their concrete type preserved."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
criterion_type: str = Field(description="Type of criterion")
|
|
66
|
+
description: str = Field(description="Description of what was checked")
|
|
67
|
+
score: float = Field(
|
|
68
|
+
ge=0.0, le=1.0, description="Continuous score from 0.0 (complete failure) to 1.0 (perfect success)"
|
|
69
|
+
)
|
|
70
|
+
details: str | None = Field(default=None, description="Additional details about the result")
|
|
71
|
+
error: str | None = Field(default=None, description="Error message if the check failed")
|
|
72
|
+
pass_threshold: float = Field(
|
|
73
|
+
default=0.9,
|
|
74
|
+
ge=0.0,
|
|
75
|
+
le=1.0,
|
|
76
|
+
description="Score required to pass this criterion (mirrors BaseSuccessCriterion.pass_threshold).",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ClassificationCriterionResult(CriterionResult):
|
|
81
|
+
"""Per-row result for classification criteria.
|
|
82
|
+
|
|
83
|
+
Carries the observed and expected labels alongside the standard score so
|
|
84
|
+
the suite-level aggregator can compute P/R/F1 and a confusion matrix.
|
|
85
|
+
Keeping these fields on a subclass (rather than the base) means results
|
|
86
|
+
for non-classification criteria don't carry dead label fields.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
result_kind: Literal["classification"] = "classification" # type: ignore[assignment]
|
|
90
|
+
observed_label: str = Field(
|
|
91
|
+
description="Observed label emitted by the criterion (sentinels like '(none)' / '(other)' allowed)."
|
|
92
|
+
)
|
|
93
|
+
expected_label: str = Field(description="Ground-truth label threaded through from the task / dataset row.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class JudgeTranscriptToolCall(BaseModel):
|
|
97
|
+
"""One tool invocation by an ``agent_judge`` sub-agent — the audit trail for one step.
|
|
98
|
+
|
|
99
|
+
Sourced from ``CommandTelemetry`` and reduced to the fields a reviewer needs
|
|
100
|
+
to verify the judge's claims (tool, target, exit status). All free-form
|
|
101
|
+
string fields go through ``scrub_reference`` before persistence.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
tool_name: str = Field(description="SDK tool name (Bash, Read, Grep, ...).")
|
|
105
|
+
detail: str = Field(
|
|
106
|
+
default="",
|
|
107
|
+
description="Per-tool target (Bash command, file path, grep pattern, ...). Truncated and scrubbed.",
|
|
108
|
+
)
|
|
109
|
+
status: str = Field(default="unknown", description="success / error / unknown — copied from telemetry.")
|
|
110
|
+
result_preview: str = Field(
|
|
111
|
+
default="",
|
|
112
|
+
description="First ~200 chars of the tool result, truncated and scrubbed.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class JudgeTranscript(BaseModel):
|
|
117
|
+
"""Captured trajectory of a judge sub-agent — written into ``JudgeCriterionResult``.
|
|
118
|
+
|
|
119
|
+
Populated by ``agent_judge`` from the returned ``TurnRecord``. ``llm_judge`` is
|
|
120
|
+
one-shot so its transcript only carries ``raw_verdict`` (the model's response),
|
|
121
|
+
``token_usage``, and the rendered prompts; ``tool_calls`` is empty there.
|
|
122
|
+
|
|
123
|
+
``judge_prompt`` and ``judge_system_prompt`` capture the exact rendered envelope
|
|
124
|
+
sent to the judge so reviewers can answer "why did the judge say X?" by replaying
|
|
125
|
+
the input post-hoc. Both go through ``scrub_reference`` before persistence so a
|
|
126
|
+
reference solution embedded via ``include_reference=true`` doesn't leak.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
tool_calls: list[JudgeTranscriptToolCall] = Field(
|
|
130
|
+
default_factory=list,
|
|
131
|
+
description="Ordered tool invocations made by the judge during evaluation.",
|
|
132
|
+
)
|
|
133
|
+
token_usage: TokenUsage | None = Field(default=None, description="Token usage for the judge's turn.")
|
|
134
|
+
duration_seconds: float = Field(default=0.0, description="Wall-clock duration of the judge's turn.")
|
|
135
|
+
raw_verdict: str = Field(
|
|
136
|
+
default="",
|
|
137
|
+
description="Scrubbed + truncated raw text of the judge's final assistant message.",
|
|
138
|
+
)
|
|
139
|
+
judge_system_prompt: str = Field(
|
|
140
|
+
default="",
|
|
141
|
+
description="Scrubbed + truncated system prompt the judge was started with (constant per criterion config).",
|
|
142
|
+
)
|
|
143
|
+
judge_prompt: str = Field(
|
|
144
|
+
default="",
|
|
145
|
+
description=(
|
|
146
|
+
"Scrubbed + truncated rendered user message — the rubric + reference + artifacts + "
|
|
147
|
+
"dialog blocks the judge actually saw. Variable per row."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
truncated: bool = Field(
|
|
151
|
+
default=False,
|
|
152
|
+
description="True when the captured transcript was clipped to fit ``max_transcript_chars``.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class JudgeCriterionResult(CriterionResult):
|
|
157
|
+
"""Per-row result for judge criteria (``llm_judge`` / ``agent_judge``).
|
|
158
|
+
|
|
159
|
+
Carries ``findings`` (bullet evidence the judge cited from the artifacts)
|
|
160
|
+
and an optional ``transcript`` so reviewers can audit the judge's verdict
|
|
161
|
+
instead of having to trust the one-line rationale alone. Subclass (rather
|
|
162
|
+
than base fields) keeps non-judge results lean.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
result_kind: Literal["judge"] = "judge" # type: ignore[assignment]
|
|
166
|
+
findings: list[str] = Field(
|
|
167
|
+
default_factory=list,
|
|
168
|
+
description="Bullet observations the judge cited from the artifacts. Scrubbed before persistence.",
|
|
169
|
+
)
|
|
170
|
+
token_usage: TokenUsage | None = Field(
|
|
171
|
+
default=None,
|
|
172
|
+
description=(
|
|
173
|
+
"Token usage for the judge's LLM call(s) — kept distinct from the "
|
|
174
|
+
"main agent's ``EvaluationResult.total_token_usage``. Populated on "
|
|
175
|
+
"all routes when the model reports usage: on DirectRoute (Anthropic) "
|
|
176
|
+
"it comes from the Anthropic response ``usage``; on Bedrock from the "
|
|
177
|
+
"``/invoke`` JSON ``usage``. ``None`` means the backend surfaced no usage (kept distinct from a "
|
|
178
|
+
"zero TokenUsage). Independent of ``capture_transcript``."
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
transcript: JudgeTranscript | None = Field(
|
|
182
|
+
default=None,
|
|
183
|
+
description=(
|
|
184
|
+
"Captured judge trajectory (tool calls + token usage + raw verdict). "
|
|
185
|
+
"None when the criterion sets ``capture_transcript=False`` or "
|
|
186
|
+
"when the judge errored before producing a turn. Held in memory "
|
|
187
|
+
"during the run for HTML rendering; excluded from ``task.json`` "
|
|
188
|
+
"via ``model_dump_json(exclude=...)`` so on-disk records carry "
|
|
189
|
+
"only ``transcript_path`` to a sibling file."
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
transcript_path: str | None = Field(
|
|
193
|
+
default=None,
|
|
194
|
+
description=(
|
|
195
|
+
"Filename of the sibling JSON file holding this result's full transcript "
|
|
196
|
+
"(e.g. ``judge-0.yaml``), relative to the directory containing ``task.json``. "
|
|
197
|
+
"Set by ``spill_judge_transcripts`` after the run; reloaded by "
|
|
198
|
+
"``load_judge_transcripts`` for re-rendering. None when no transcript was captured."
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Criterion types whose results are ``JudgeCriterionResult`` / ``ClassificationCriterionResult``.
|
|
204
|
+
# Used by ``_criterion_result_discriminator`` to type-infer legacy ``task.json`` records that
|
|
205
|
+
# lack the ``result_kind`` field. Listed here (not on each criterion class) because
|
|
206
|
+
# ``model_validate_json`` runs without the criteria registry loaded — consumers that import
|
|
207
|
+
# ``coder_eval.models`` to deserialize ``task.json`` (e.g. ``coder-eval report``) do NOT
|
|
208
|
+
# import ``coder_eval.criteria.*`` (which is where checkers self-register). Putting the
|
|
209
|
+
# inference rule on criterion classes would force importing every checker just to deserialize
|
|
210
|
+
# a row record, and would create a circular dependency since ``criteria/*.py`` already imports
|
|
211
|
+
# from this module.
|
|
212
|
+
#
|
|
213
|
+
# Convention: when a new criterion type needs a non-``"basic"`` result class, add its
|
|
214
|
+
# ``criterion_type`` value to the matching frozenset in the same PR that introduces the
|
|
215
|
+
# criterion.
|
|
216
|
+
_JUDGE_CRITERION_TYPES = frozenset({"llm_judge", "agent_judge"})
|
|
217
|
+
_CLASSIFICATION_CRITERION_TYPES = frozenset({"classification_match", "skill_triggered"})
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _criterion_result_discriminator(v: Any) -> str:
|
|
221
|
+
"""Dispatch ``CriterionResult`` subclasses for serialization and validation.
|
|
222
|
+
|
|
223
|
+
Prefers explicit ``result_kind`` (written by current code); falls back to
|
|
224
|
+
inference from ``criterion_type`` for legacy task.json files written before
|
|
225
|
+
this field existed. Unknown criterion_types fall through to ``"basic"``.
|
|
226
|
+
|
|
227
|
+
Explicit ``result_kind`` wins over ``criterion_type`` inference: a dict
|
|
228
|
+
carrying ``result_kind="basic"`` with ``criterion_type="llm_judge"`` is
|
|
229
|
+
routed to the base class. This matters for forward-compat experiments where
|
|
230
|
+
a writer deliberately downgrades a result shape.
|
|
231
|
+
"""
|
|
232
|
+
if isinstance(v, dict):
|
|
233
|
+
kind = v.get("result_kind")
|
|
234
|
+
if kind:
|
|
235
|
+
return str(kind)
|
|
236
|
+
ct = v.get("criterion_type", "")
|
|
237
|
+
if ct in _JUDGE_CRITERION_TYPES:
|
|
238
|
+
return "judge"
|
|
239
|
+
if ct in _CLASSIFICATION_CRITERION_TYPES:
|
|
240
|
+
return "classification"
|
|
241
|
+
return "basic"
|
|
242
|
+
# Pydantic instance — read the discriminator field.
|
|
243
|
+
return getattr(v, "result_kind", "basic")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
CriterionResultUnion = Annotated[
|
|
247
|
+
(
|
|
248
|
+
Annotated[CriterionResult, Tag("basic")]
|
|
249
|
+
| Annotated[JudgeCriterionResult, Tag("judge")]
|
|
250
|
+
| Annotated[ClassificationCriterionResult, Tag("classification")]
|
|
251
|
+
),
|
|
252
|
+
Discriminator(_criterion_result_discriminator),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ResultSummary(BaseModel):
|
|
257
|
+
"""Diagnostic fields lifted from the SDK's final ResultMessage.
|
|
258
|
+
|
|
259
|
+
Powers the agent's debug log and the error-path formatter that
|
|
260
|
+
surfaces a useful detail string when the CLI crashes. Persisted on
|
|
261
|
+
``TurnRecord`` for clean turns only — on a crash the agent raises
|
|
262
|
+
before the TurnRecord is constructed, so post-mortem persistence on
|
|
263
|
+
error turns is out of scope here.
|
|
264
|
+
|
|
265
|
+
Mirrors the diagnostic-bearing subset of
|
|
266
|
+
``claude_agent_sdk.ResultMessage``; pure accounting fields
|
|
267
|
+
(``num_turns``, ``duration_ms``) live on ``TurnRecord`` /
|
|
268
|
+
``TokenUsage``.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
is_error: bool = Field(description="Whether the SDK reported the turn as errored")
|
|
272
|
+
subtype: str = Field(description="Coarse classification (e.g. 'success', 'error_during_execution')")
|
|
273
|
+
stop_reason: str | None = Field(default=None, description="Why the model stopped, if reported")
|
|
274
|
+
result: str | None = Field(default=None, description="Free-form result/error text from the SDK")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TurnRecord(BaseModel):
|
|
278
|
+
"""Record of a single agent turn (input + output)."""
|
|
279
|
+
|
|
280
|
+
iteration: int = Field(
|
|
281
|
+
description=(
|
|
282
|
+
"Orchestrator iteration number. Multiple records may share this value when "
|
|
283
|
+
"crashed=True partials precede a retry; use the list index in "
|
|
284
|
+
"EvaluationResult.iterations as the canonical unique key."
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
user_input: str = Field(description="Input prompt to the agent")
|
|
288
|
+
agent_output: str = Field(description="Agent's response (legacy format)")
|
|
289
|
+
commands: list[CommandTelemetry] = Field(
|
|
290
|
+
default_factory=list, description="Detailed telemetry for each command executed during this turn"
|
|
291
|
+
)
|
|
292
|
+
timestamp: datetime = Field(default_factory=datetime.now, description="When this turn occurred")
|
|
293
|
+
duration_seconds: float = Field(default=0.0, description="How long this turn took")
|
|
294
|
+
token_usage: TokenUsage | None = Field(
|
|
295
|
+
default=None, description="Token usage for this turn (if available from agent SDK)"
|
|
296
|
+
)
|
|
297
|
+
model_used: str | None = Field(
|
|
298
|
+
default=None, description="Model identifier used for this turn (e.g., 'claude-sonnet-4-5-20250514')"
|
|
299
|
+
)
|
|
300
|
+
assistant_turn_count: int = Field(
|
|
301
|
+
default=0,
|
|
302
|
+
description="Number of AssistantMessage objects received from the SDK in this turn.",
|
|
303
|
+
)
|
|
304
|
+
messages: list[TranscriptMessage] = Field(
|
|
305
|
+
default_factory=list,
|
|
306
|
+
description=(
|
|
307
|
+
"Per-message telemetry in emission order, mirroring the LLM API messages array. "
|
|
308
|
+
"Includes UserMessage entries (simulator text or tool results), AssistantMessage entries "
|
|
309
|
+
"(agent thinking/tool_use/text blocks), and an optional terminal ReconciliationMessage "
|
|
310
|
+
"carrying tokens the agent billed but never surfaced as a generation (so the transcript's "
|
|
311
|
+
"token buckets sum to token_usage). Preserves the full conversation trajectory for "
|
|
312
|
+
"replay and analysis. May be empty for agents/modes that don't surface message detail."
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
num_turns: int | None = Field(
|
|
316
|
+
default=None,
|
|
317
|
+
description=(
|
|
318
|
+
"Number of inner-loop turns the SDK reported for this communicate() call "
|
|
319
|
+
"(from ResultMessage.num_turns). None when the SDK did not emit a "
|
|
320
|
+
"ResultMessage (e.g. crash partial before the final message arrived)."
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
max_turns_exhausted: bool = Field(
|
|
324
|
+
default=False,
|
|
325
|
+
description="Whether the agent hit the max_turns limit without voluntarily completing",
|
|
326
|
+
)
|
|
327
|
+
result_summary: ResultSummary | None = Field(
|
|
328
|
+
default=None,
|
|
329
|
+
description="SDK ResultMessage summary, when one was emitted (clean turns or partials that got one).",
|
|
330
|
+
)
|
|
331
|
+
crashed: bool = Field(
|
|
332
|
+
default=False,
|
|
333
|
+
description=(
|
|
334
|
+
"True if the agent failed mid-turn. Pre-failure commands/files/output are real work "
|
|
335
|
+
"and are counted by aggregators; the retry's API call is billed separately."
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
crash_reason: str | None = Field(
|
|
339
|
+
default=None,
|
|
340
|
+
description="Short human-readable cause when crashed=True; None otherwise.",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class PostRunResult(BaseModel):
|
|
345
|
+
"""Result of executing a single post-run command."""
|
|
346
|
+
|
|
347
|
+
command: str = Field(description="Command as specified in the task definition")
|
|
348
|
+
exit_code: int | None = Field(default=None, description="Process exit code (None if timed out or failed to start)")
|
|
349
|
+
stdout: str = Field(default="", description="Captured stdout")
|
|
350
|
+
stderr: str = Field(default="", description="Captured stderr")
|
|
351
|
+
duration_seconds: float = Field(default=0.0, description="Execution duration in seconds")
|
|
352
|
+
error: str | None = Field(default=None, description="Error message if the command failed to execute")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class SimulationTelemetry(BaseModel):
|
|
356
|
+
"""Per-trial simulation telemetry.
|
|
357
|
+
|
|
358
|
+
Populated only when ``TaskDefinition.simulation.enabled`` is True. One
|
|
359
|
+
``EvaluationResult`` corresponds to one dialog trajectory, so this record
|
|
360
|
+
is also per-dialog.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
model_config = {"extra": "forbid"}
|
|
364
|
+
|
|
365
|
+
n_trials: int = Field(description="Total number of trials the task was expanded into", ge=1)
|
|
366
|
+
replicate_index: int = Field(description="Zero-indexed trial number within this (task, variant)", ge=0)
|
|
367
|
+
stop_reason: Literal[
|
|
368
|
+
"criteria_passed",
|
|
369
|
+
"stop_token",
|
|
370
|
+
"max_turns",
|
|
371
|
+
"budget",
|
|
372
|
+
"error",
|
|
373
|
+
"run_limit_exceeded",
|
|
374
|
+
] = Field(description="Why the dialog terminated")
|
|
375
|
+
simulator_input_tokens: int = Field(default=0, ge=0, description="Sum of simulator prompt tokens across turns")
|
|
376
|
+
simulator_output_tokens: int = Field(default=0, ge=0, description="Sum of simulator completion tokens across turns")
|
|
377
|
+
simulator_failures: int = Field(default=0, ge=0, description="Number of simulator LLM calls that raised")
|
|
378
|
+
total_turns: int = Field(description="Number of user↔agent exchanges completed in this dialog", ge=0)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class EvaluationResult(BaseModel):
|
|
382
|
+
"""Complete result of a task evaluation."""
|
|
383
|
+
|
|
384
|
+
task_id: str = Field(description="ID of the evaluated task")
|
|
385
|
+
task_description: str = Field(description="Description of the task")
|
|
386
|
+
variant_id: str = Field(default="default", description="ID of the experiment variant")
|
|
387
|
+
agent_type: str = Field(description="Type of agent used (registered kind string, e.g. 'claude-code')")
|
|
388
|
+
model_used: str | None = Field(
|
|
389
|
+
default=None, description="Model identifier used for the evaluation (resolved from iterations or agent config)"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Execution metadata
|
|
393
|
+
started_at: datetime = Field(description="When evaluation started")
|
|
394
|
+
completed_at: datetime | None = Field(default=None, description="When evaluation completed")
|
|
395
|
+
duration_seconds: float = Field(default=0.0, description="Total evaluation duration")
|
|
396
|
+
|
|
397
|
+
# Results
|
|
398
|
+
final_status: FinalStatus = Field(description="Final status of the evaluation")
|
|
399
|
+
max_turns_exhausted: bool = Field(
|
|
400
|
+
default=False,
|
|
401
|
+
description="Whether any iteration hit the agent max_turns limit without the agent voluntarily completing",
|
|
402
|
+
)
|
|
403
|
+
weighted_score: float | None = Field(
|
|
404
|
+
default=None, ge=0.0, le=1.0, description="Weighted average of criterion scores (0.0 to 1.0)"
|
|
405
|
+
)
|
|
406
|
+
iteration_count: int = Field(description="Number of iterations completed")
|
|
407
|
+
success_criteria_results: list[CriterionResultUnion] = Field(
|
|
408
|
+
default_factory=list,
|
|
409
|
+
description=(
|
|
410
|
+
"Results of all success criteria checks. Typed as the ``CriterionResultUnion`` "
|
|
411
|
+
"discriminated union so subclass-specific fields (``JudgeCriterionResult.findings`` / "
|
|
412
|
+
"``transcript``, ``ClassificationCriterionResult.observed_label``) round-trip "
|
|
413
|
+
"concretely through ``model_dump_json`` → ``model_validate_json``. Legacy task.json "
|
|
414
|
+
"files without ``result_kind`` are inferred from ``criterion_type``."
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Detailed transcript
|
|
419
|
+
iterations: list[TurnRecord] = Field(
|
|
420
|
+
default_factory=list,
|
|
421
|
+
description="Complete transcript of agent interactions across all iterations (task attempts)",
|
|
422
|
+
validation_alias=AliasChoices("iterations", "turns"),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Error information
|
|
426
|
+
error_message: str | None = Field(default=None, description="Error message if evaluation failed")
|
|
427
|
+
error_details: dict[str, Any] | None = Field(
|
|
428
|
+
default=None, description="Detailed error context from error_handling module"
|
|
429
|
+
)
|
|
430
|
+
error_log_tail: str | None = Field(
|
|
431
|
+
default=None,
|
|
432
|
+
description=(
|
|
433
|
+
"Sanitised tail of task.log captured during the run, populated for failure "
|
|
434
|
+
"statuses (ERROR, BUILD_FAILED, TIMEOUT, FAILURE, and the budget-exceeded "
|
|
435
|
+
"statuses). For BUILD_FAILED it carries the `docker build` log tail. "
|
|
436
|
+
"Used by the HTML report's Logs disclosure."
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Environment information
|
|
441
|
+
environment_info: dict[str, Any] = Field(
|
|
442
|
+
default_factory=dict, description="Version information and environment details"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Agent configuration. ResolvedAgentConfig (base-typed + SerializeAsAny + registry
|
|
446
|
+
# coercion) so a plugin kind's subclass-only fields survive the dump to task.json
|
|
447
|
+
# and reload — the same round-trip guarantee TaskDefinition.agent has.
|
|
448
|
+
agent_config: ResolvedAgentConfig | None = Field(
|
|
449
|
+
default=None,
|
|
450
|
+
description="Agent configuration used for the evaluation (from task YAML)",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# SDK options (raw dump of all ClaudeAgentOptions fields including defaults)
|
|
454
|
+
sdk_options: dict[str, Any] | None = Field(
|
|
455
|
+
default=None,
|
|
456
|
+
description="Raw SDK options dump from ClaudeAgentOptions (all fields including defaults)",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Artifacts
|
|
460
|
+
sandbox_path: str | None = Field(default=None, description="Path to preserved sandbox (if saved)")
|
|
461
|
+
|
|
462
|
+
# Full task configuration (resolved config + source YAML + lineage)
|
|
463
|
+
task_config: TaskConfigRecord | None = Field(default=None, description="Full task configuration snapshot")
|
|
464
|
+
|
|
465
|
+
# Command telemetry
|
|
466
|
+
command_stats: CommandStatistics | None = Field(default=None, description="Aggregated command telemetry statistics")
|
|
467
|
+
|
|
468
|
+
# Token usage
|
|
469
|
+
total_token_usage: TokenUsage | None = Field(default=None, description="Aggregated token usage across all turns")
|
|
470
|
+
|
|
471
|
+
# Assistant turns
|
|
472
|
+
total_assistant_turns: int | None = Field(
|
|
473
|
+
default=None,
|
|
474
|
+
description="Total assistant turns across all orchestrator iterations",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Commands efficiency (orchestrator-level tracking)
|
|
478
|
+
expected_commands: int | None = Field(default=None, description="Expected commands from task definition")
|
|
479
|
+
actual_commands: int | None = Field(default=None, description="Actual tool commands executed by the agent")
|
|
480
|
+
commands_efficiency: float | None = Field(
|
|
481
|
+
default=None, description="Commands efficiency score (0-1). expected/max(actual, expected)"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Pre-run script results
|
|
485
|
+
pre_run_results: list[PostRunResult] = Field(
|
|
486
|
+
default_factory=list,
|
|
487
|
+
description=(
|
|
488
|
+
"Results of pre-run scripts. "
|
|
489
|
+
"A failed command with fail_on_error=True aborts evaluation; the result is still captured here."
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Post-run script results
|
|
494
|
+
post_run_results: list[PostRunResult] = Field(
|
|
495
|
+
default_factory=list, description="Results of post-run scripts (informational, do not affect pass/fail)"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Simulation telemetry (only populated when task.simulation.enabled is True)
|
|
499
|
+
simulation: SimulationTelemetry | None = Field(
|
|
500
|
+
default=None,
|
|
501
|
+
description=(
|
|
502
|
+
"Dialog-simulation telemetry: trial index, stop reason, simulator token usage. None in single-shot mode."
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def calculate_weighted_score(self, criteria: list[SuccessCriterion]) -> None:
|
|
507
|
+
"""Calculate weighted average score from criterion results.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
criteria: Original criterion definitions with weights
|
|
511
|
+
|
|
512
|
+
This method mutates self.weighted_score.
|
|
513
|
+
"""
|
|
514
|
+
if not self.success_criteria_results or not criteria:
|
|
515
|
+
self.weighted_score = 0.0
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
if len(self.success_criteria_results) != len(criteria):
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"Results/criteria length mismatch: {len(self.success_criteria_results)} results "
|
|
521
|
+
+ f"vs {len(criteria)} criteria for task {self.task_id}. This indicates an upstream "
|
|
522
|
+
+ "bug; refusing to fabricate a weight-ignoring score."
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
total_weighted_score = 0.0
|
|
526
|
+
total_weight = 0.0
|
|
527
|
+
|
|
528
|
+
for result, criterion in zip(self.success_criteria_results, criteria, strict=True):
|
|
529
|
+
total_weighted_score += result.score * criterion.weight
|
|
530
|
+
total_weight += criterion.weight
|
|
531
|
+
|
|
532
|
+
self.weighted_score = total_weighted_score / total_weight if total_weight > 0 else 0.0
|
|
533
|
+
|
|
534
|
+
def all_criteria_passed(self, criteria: list[SuccessCriterion]) -> bool:
|
|
535
|
+
"""True iff every criterion result meets its pass_threshold.
|
|
536
|
+
|
|
537
|
+
Single source of truth for the success gate. A results/criteria length
|
|
538
|
+
mismatch raises ``ValueError`` rather than silently truncating — the
|
|
539
|
+
``len()`` pre-check is required because ``all()`` short-circuits on the
|
|
540
|
+
first failing pair, so ``zip(strict=True)`` alone would not reliably
|
|
541
|
+
reach the length check.
|
|
542
|
+
"""
|
|
543
|
+
if len(self.success_criteria_results) != len(criteria):
|
|
544
|
+
raise ValueError(
|
|
545
|
+
f"Results/criteria length mismatch for task {self.task_id}: "
|
|
546
|
+
+ f"{len(self.success_criteria_results)} results vs {len(criteria)} criteria."
|
|
547
|
+
)
|
|
548
|
+
return all(r.score >= c.pass_threshold for r, c in zip(self.success_criteria_results, criteria, strict=True))
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class CriterionStats(BaseModel):
|
|
552
|
+
"""Per-criterion-type aggregate stats across a suite's rows."""
|
|
553
|
+
|
|
554
|
+
criterion_type: str = Field(description="Criterion type (e.g., 'file_exists')")
|
|
555
|
+
rows_evaluated: int = Field(description="Rows where this criterion appeared at least once")
|
|
556
|
+
average_score: float = Field(ge=0.0, le=1.0, description="Mean score across all evaluations of this criterion")
|
|
557
|
+
error_count: int = Field(default=0, description="Evaluations that surfaced a checker-level error")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class FailedRowSummary(BaseModel):
|
|
561
|
+
"""Per-row summary for a row that did not succeed — used for error-sample rendering."""
|
|
562
|
+
|
|
563
|
+
row_id: str | None = Field(description="Row id within the suite (None if unavailable)")
|
|
564
|
+
task_id: str = Field(description="Full task id (suite_id/row_id)")
|
|
565
|
+
final_status: FinalStatus = Field(description="Final status of this row")
|
|
566
|
+
weighted_score: float | None = Field(default=None, description="Weighted score (None on error)")
|
|
567
|
+
failure_reasons: list[str] = Field(
|
|
568
|
+
default_factory=list,
|
|
569
|
+
description="Short descriptions of failed criteria (up to a few, truncated per row).",
|
|
570
|
+
)
|
|
571
|
+
error_message: str | None = Field(default=None, description="Top-level error message if the row errored out.")
|
|
572
|
+
task_json_relpath: str = Field(
|
|
573
|
+
description="Path to the row's task.json, relative to run_dir (<variant>/<task_id>/<NN>/task.json)."
|
|
574
|
+
)
|
|
575
|
+
replicate_index: int = Field(
|
|
576
|
+
default=0,
|
|
577
|
+
ge=0,
|
|
578
|
+
description="Replicate index of this failed row (0 when repeats disabled).",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class ClassLabelStats(BaseModel):
|
|
583
|
+
"""Per-class precision / recall / F1. Used by classification_match's aggregate details."""
|
|
584
|
+
|
|
585
|
+
label: str = Field(description="Class label (may be a sentinel like '(none)' / '(other)')")
|
|
586
|
+
precision: float = Field(ge=0.0, le=1.0, description="TP / (TP + FP); 0.0 when denom is 0")
|
|
587
|
+
recall: float = Field(ge=0.0, le=1.0, description="TP / (TP + FN); 0.0 when denom is 0")
|
|
588
|
+
f1: float = Field(ge=0.0, le=1.0, description="Harmonic mean of precision and recall; 0.0 when both are 0")
|
|
589
|
+
support: int = Field(ge=0, description="Rows where expected_label == this class")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class ConfusionEntry(BaseModel):
|
|
593
|
+
"""A single cell of the confusion matrix. Used by classification_match's aggregate details."""
|
|
594
|
+
|
|
595
|
+
expected: str
|
|
596
|
+
observed: str
|
|
597
|
+
count: int = Field(ge=0)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class ThresholdCheck(BaseModel):
|
|
601
|
+
"""Pass/fail for a single {metric: min_value} threshold at suite level."""
|
|
602
|
+
|
|
603
|
+
metric: str = Field(description="Metric name as emitted by the criterion's aggregate() output.")
|
|
604
|
+
min_value: float = Field(description="Configured minimum. The check passes when actual_value >= min_value.")
|
|
605
|
+
actual_value: float | None = Field(
|
|
606
|
+
default=None,
|
|
607
|
+
description="Observed metric value. None when the aggregator did not emit this metric.",
|
|
608
|
+
)
|
|
609
|
+
passed: bool
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class CriterionAggregate(BaseModel):
|
|
613
|
+
"""Across-row aggregate produced by a criterion's aggregate() method.
|
|
614
|
+
|
|
615
|
+
Each success_criterion on a dataset-backed task can emit one of these. The
|
|
616
|
+
``metrics`` dict holds flat named values (e.g. ``'accuracy'``, ``'f1.macro'``,
|
|
617
|
+
``'recall.positive'``) that ``suite_thresholds`` on the criterion reference.
|
|
618
|
+
``details`` carries shape-specific extras for rendering — for
|
|
619
|
+
``classification_match`` that's labels, per-label stats, and the confusion
|
|
620
|
+
matrix.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
criterion_type: str
|
|
624
|
+
description: str | None = Field(
|
|
625
|
+
default=None,
|
|
626
|
+
description=(
|
|
627
|
+
"The source criterion's description. Set when a task stacks multiple criteria of the "
|
|
628
|
+
"same type (e.g. activation's per-skill skill_triggered criteria) so each aggregate is "
|
|
629
|
+
"distinguishable in the rollup; None for single-criterion-per-type suites."
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
rows_total: int = Field(
|
|
633
|
+
default=0,
|
|
634
|
+
ge=0,
|
|
635
|
+
description=(
|
|
636
|
+
"Total rows in the suite for this variant — the denominator a reader expects. 0 when not "
|
|
637
|
+
"computed at suite level (e.g. a criterion-level aggregate() called outside a rollup)."
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
rows_excluded: int = Field(
|
|
641
|
+
default=0,
|
|
642
|
+
ge=0,
|
|
643
|
+
description=(
|
|
644
|
+
"Rows dropped from this aggregate's per-row slice because they produced no result at this "
|
|
645
|
+
"criterion's position (e.g. ERROR rows landing before criteria ran). The classification/score "
|
|
646
|
+
"denominator is rows_total - rows_excluded."
|
|
647
|
+
),
|
|
648
|
+
)
|
|
649
|
+
metrics: dict[str, float] = Field(default_factory=dict, description="Flat metric name -> value")
|
|
650
|
+
threshold_checks: list[ThresholdCheck] = Field(default_factory=list)
|
|
651
|
+
passed: bool = Field(description="True when all threshold_checks passed (trivially true when no thresholds).")
|
|
652
|
+
details: dict[str, Any] = Field(
|
|
653
|
+
default_factory=dict,
|
|
654
|
+
description="Criterion-specific structured data used for markdown rendering (e.g. confusion matrix).",
|
|
655
|
+
)
|
|
656
|
+
error: str | None = Field(
|
|
657
|
+
default=None,
|
|
658
|
+
description="Populated when the criterion declared suite_thresholds but aggregate() returned nothing.",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class SuiteRollup(BaseModel):
|
|
663
|
+
"""Pass-rate rollup for a dataset-backed suite under a single variant.
|
|
664
|
+
|
|
665
|
+
Written to ``<run_dir>/<variant_id>/<suite_id>/suite.json`` and
|
|
666
|
+
``suite.md`` next to the per-row directories. Presence is gated on
|
|
667
|
+
the task having been expanded from a Dataset.
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
suite_id: str
|
|
671
|
+
variant_id: str
|
|
672
|
+
rows_total: int
|
|
673
|
+
rows_passed: int
|
|
674
|
+
rows_failed: int
|
|
675
|
+
rows_error: int
|
|
676
|
+
pass_rate: float = Field(ge=0.0, le=1.0, description="rows_passed / rows_total")
|
|
677
|
+
average_weighted_score: float | None = Field(
|
|
678
|
+
default=None, description="Mean weighted_score across rows that produced one."
|
|
679
|
+
)
|
|
680
|
+
criterion_stats: list[CriterionStats] = Field(default_factory=list)
|
|
681
|
+
failed_samples: list[FailedRowSummary] = Field(
|
|
682
|
+
default_factory=list,
|
|
683
|
+
description="Up to K failed/errored rows with failure reasons for error analysis.",
|
|
684
|
+
)
|
|
685
|
+
criterion_aggregates: list[CriterionAggregate] = Field(
|
|
686
|
+
default_factory=list,
|
|
687
|
+
description=(
|
|
688
|
+
"One entry per criterion_type whose aggregate() returned a result. "
|
|
689
|
+
"Empty when no criterion opted into across-row aggregation."
|
|
690
|
+
),
|
|
691
|
+
)
|
|
692
|
+
passed: bool = Field(
|
|
693
|
+
default=True,
|
|
694
|
+
description=(
|
|
695
|
+
"True when every criterion_aggregate passed its thresholds (or none had thresholds). "
|
|
696
|
+
"Drives CLI exit code for dataset-backed tasks."
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class SkippedTask(BaseModel):
|
|
702
|
+
"""A task YAML that was excluded from the run before reaching the orchestrator.
|
|
703
|
+
|
|
704
|
+
Recorded by ``resolve_all_tasks`` in two cases:
|
|
705
|
+
|
|
706
|
+
* **Load failure** — ``load_task`` (YAML parse / Pydantic validation) or
|
|
707
|
+
``expand_dataset`` raised. ``reason`` carries the exception type + message.
|
|
708
|
+
* **Intentional opt-out** — the YAML set ``skip: true``. ``reason`` is
|
|
709
|
+
prefixed ``"skip: true"`` so consumers can distinguish opt-outs from errors.
|
|
710
|
+
|
|
711
|
+
Either way the run continues with the remaining tasks; the suite is not
|
|
712
|
+
aborted by a single bad file or quarantined task.
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
path: str = Field(description="Absolute path to the task YAML that was excluded.")
|
|
716
|
+
reason: str = Field(
|
|
717
|
+
description=(
|
|
718
|
+
"Short reason — exception type + message for load failures, or "
|
|
719
|
+
"``'skip: true (task_id=...)'`` for intentional opt-outs."
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class RunSummary(BaseModel):
|
|
725
|
+
"""Summary of an entire evaluation run across multiple tasks."""
|
|
726
|
+
|
|
727
|
+
run_id: str = Field(description="Run identifier (timestamp like '2025-10-09_15-30-45')")
|
|
728
|
+
start_time: datetime = Field(description="Run start time")
|
|
729
|
+
end_time: datetime = Field(description="Run end time")
|
|
730
|
+
total_duration_seconds: float = Field(description="Total duration of the run in seconds")
|
|
731
|
+
|
|
732
|
+
# Task statistics
|
|
733
|
+
tasks_run: int = Field(description="Total number of tasks executed")
|
|
734
|
+
tasks_succeeded: int = Field(description="Number of tasks that succeeded")
|
|
735
|
+
tasks_failed: int = Field(description="Number of tasks that failed")
|
|
736
|
+
tasks_error: int = Field(description="Number of tasks that encountered errors")
|
|
737
|
+
|
|
738
|
+
# Informational sub-counters: subsets of tasks_failed (NOT part of the
|
|
739
|
+
# task_count invariant). Default 0 so old serialized RunSummary JSON
|
|
740
|
+
# without these fields deserialises cleanly.
|
|
741
|
+
tasks_token_budget_exceeded: int = Field(
|
|
742
|
+
default=0,
|
|
743
|
+
ge=0,
|
|
744
|
+
description="Subset of tasks_failed where run_limits token caps tripped.",
|
|
745
|
+
)
|
|
746
|
+
tasks_cost_budget_exceeded: int = Field(
|
|
747
|
+
default=0,
|
|
748
|
+
ge=0,
|
|
749
|
+
description="Subset of tasks_failed where run_limits cost cap tripped.",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Tasks excluded at resolution time — either load failures (YAML / schema
|
|
753
|
+
# errors) or intentional opt-outs (``skip: true``). Distinct from
|
|
754
|
+
# tasks_error: these never reached the orchestrator. Empty for runs
|
|
755
|
+
# where every task YAML loaded cleanly and none opted out.
|
|
756
|
+
skipped_tasks: list[SkippedTask] = Field(
|
|
757
|
+
default_factory=list,
|
|
758
|
+
description=(
|
|
759
|
+
"Task YAMLs excluded from execution — either failed schema "
|
|
760
|
+
"validation or opted out via ``skip: true``. See SkippedTask.reason."
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# Configured concurrency (BatchRunConfig.max_parallel). Defaulted so existing
|
|
765
|
+
# callers and fixtures don't have to set it.
|
|
766
|
+
max_parallel: int = Field(default=1, ge=1, description="Configured max concurrent tasks for this run")
|
|
767
|
+
|
|
768
|
+
# Detailed results
|
|
769
|
+
task_results: list[dict[str, Any]] = Field(description="List of task results with {task_id, status, duration}")
|
|
770
|
+
|
|
771
|
+
# Environment info
|
|
772
|
+
framework_version: str = Field(description="Version of coder_eval framework")
|
|
773
|
+
environment_info: dict[str, Any] = Field(
|
|
774
|
+
default_factory=dict,
|
|
775
|
+
description=(
|
|
776
|
+
"Environment and dependency versions. Values are usually strings but may be "
|
|
777
|
+
"nested (e.g. ``tool_plugins`` is a ``{plugin: version}`` dict), so the value "
|
|
778
|
+
"type is ``Any`` to match ``EvaluationResult.environment_info``."
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
@model_validator(mode="after")
|
|
783
|
+
def _check_task_count_invariant(self) -> RunSummary:
|
|
784
|
+
if self.tasks_succeeded + self.tasks_failed + self.tasks_error != self.tasks_run:
|
|
785
|
+
total = f"{self.tasks_succeeded} + {self.tasks_failed} + {self.tasks_error}"
|
|
786
|
+
raise ValueError(f"Task count invariant violated: {total} != {self.tasks_run}")
|
|
787
|
+
return self
|