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,454 @@
|
|
|
1
|
+
"""Telemetry and command statistics 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 BaseModel, Discriminator, Field, computed_field, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenUsage(BaseModel):
|
|
12
|
+
"""Token usage from a single agent turn or aggregated across turns.
|
|
13
|
+
|
|
14
|
+
The prompt is split into three mutually-exclusive buckets that SUM to the
|
|
15
|
+
full prompt: ``uncached_input_tokens`` (fresh, billed at the input rate),
|
|
16
|
+
``cache_creation_input_tokens`` (written to cache), and
|
|
17
|
+
``cache_read_input_tokens`` (served from cache). ``input_tokens`` is the
|
|
18
|
+
DERIVED total of the three (the whole prompt the model processed), exposed
|
|
19
|
+
as a computed field so it can never drift from its parts.
|
|
20
|
+
|
|
21
|
+
Provider mapping for ``uncached_input_tokens``:
|
|
22
|
+
|
|
23
|
+
* Anthropic / Claude: the API ``input_tokens`` (the uncached slice);
|
|
24
|
+
``cache_creation``/``cache_read`` as reported.
|
|
25
|
+
* OpenAI / Codex: ``input - cached`` (the fresh slice). ``cache_creation`` is
|
|
26
|
+
0 — OpenAI bills no separate cache-write fee — and ``cache_read`` is the
|
|
27
|
+
cached prefix.
|
|
28
|
+
|
|
29
|
+
COST bills ``uncached_input_tokens`` at the input rate, NOT ``input_tokens``
|
|
30
|
+
(which already contains the cache buckets and would double-count them).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@model_validator(mode="before")
|
|
34
|
+
@classmethod
|
|
35
|
+
def _adopt_legacy_input_tokens(cls, data: Any) -> Any:
|
|
36
|
+
"""Read pre-split ``task.json`` files without silently under-counting.
|
|
37
|
+
|
|
38
|
+
Before the uncached/derived split, the stored field was ``input_tokens``
|
|
39
|
+
and it carried the FULL prompt. It is now a computed field, so a naive
|
|
40
|
+
reload drops it and recomputes from the cache buckets only — turning a
|
|
41
|
+
stored ``input_tokens=1000`` into ``350``. When the new stored field
|
|
42
|
+
``uncached_input_tokens`` is absent but a legacy ``input_tokens`` is
|
|
43
|
+
present, adopt the legacy value as ``uncached_input_tokens`` so the Python
|
|
44
|
+
read path agrees with the evalboard (which already falls back the same
|
|
45
|
+
way). Current-format payloads carry ``uncached_input_tokens`` and are
|
|
46
|
+
untouched.
|
|
47
|
+
"""
|
|
48
|
+
if isinstance(data, dict) and "uncached_input_tokens" not in data and "input_tokens" in data:
|
|
49
|
+
data = {**data, "uncached_input_tokens": data["input_tokens"]}
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
uncached_input_tokens: int = Field(
|
|
53
|
+
default=0,
|
|
54
|
+
description=(
|
|
55
|
+
"Fresh (uncached) input prompt tokens, billed at the input rate. Maps to "
|
|
56
|
+
"Anthropic's API input_tokens; for Codex == (input - cached). This is the "
|
|
57
|
+
"field cost is computed from, NOT input_tokens (the derived total)."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
output_tokens: int = Field(default=0, description="Output completion tokens")
|
|
61
|
+
cache_creation_input_tokens: int = Field(
|
|
62
|
+
default=0,
|
|
63
|
+
description="Tokens written to the prompt cache this call (Anthropic only; 0 for Codex).",
|
|
64
|
+
)
|
|
65
|
+
cache_read_input_tokens: int = Field(default=0, description="Tokens read from prompt cache")
|
|
66
|
+
total_cost_usd: float | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description=(
|
|
69
|
+
"Total cost in USD. Normally the agent SDK's billed total; on a "
|
|
70
|
+
"timed-out/killed turn (no terminal result message) it is backfilled "
|
|
71
|
+
"from the rate card via pricing.calculate_cost, so a multi-turn "
|
|
72
|
+
"or run total may blend SDK-billed and rate-card-estimated figures."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@computed_field # type: ignore[prop-decorator]
|
|
77
|
+
@property
|
|
78
|
+
def input_tokens(self) -> int:
|
|
79
|
+
"""Total prompt tokens = uncached + cache_creation + cache_read.
|
|
80
|
+
|
|
81
|
+
Derived (read-only) so it always equals the sum of its parts. This is the
|
|
82
|
+
whole prompt the model processed, NOT the billable-at-input-rate slice —
|
|
83
|
+
for cost use ``uncached_input_tokens``.
|
|
84
|
+
"""
|
|
85
|
+
return self.uncached_input_tokens + self.cache_creation_input_tokens + self.cache_read_input_tokens
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def total_tokens(self) -> int:
|
|
89
|
+
"""All tokens the turn processed: full prompt (input_tokens) + output."""
|
|
90
|
+
return self.input_tokens + self.output_tokens
|
|
91
|
+
|
|
92
|
+
def is_empty(self) -> bool:
|
|
93
|
+
"""True when every token counter is zero (cost is ignored).
|
|
94
|
+
|
|
95
|
+
Used to decide whether SDK-parsed usage carries any real traffic
|
|
96
|
+
before attributing it to a turn/judge.
|
|
97
|
+
"""
|
|
98
|
+
return (
|
|
99
|
+
self.uncached_input_tokens == 0
|
|
100
|
+
and self.output_tokens == 0
|
|
101
|
+
and self.cache_creation_input_tokens == 0
|
|
102
|
+
and self.cache_read_input_tokens == 0
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __add__(self, other: TokenUsage) -> TokenUsage:
|
|
106
|
+
"""Field-wise sum. Cost is summed only over the non-None operands
|
|
107
|
+
(stays None when neither carries a cost). ``input_tokens`` is derived, so
|
|
108
|
+
summing the parts keeps the total consistent automatically."""
|
|
109
|
+
costs = [c for c in (self.total_cost_usd, other.total_cost_usd) if c is not None]
|
|
110
|
+
return TokenUsage(
|
|
111
|
+
uncached_input_tokens=self.uncached_input_tokens + other.uncached_input_tokens,
|
|
112
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
113
|
+
cache_creation_input_tokens=self.cache_creation_input_tokens + other.cache_creation_input_tokens,
|
|
114
|
+
cache_read_input_tokens=self.cache_read_input_tokens + other.cache_read_input_tokens,
|
|
115
|
+
total_cost_usd=sum(costs) if costs else None,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ContentBlock(BaseModel):
|
|
120
|
+
"""One content block within a message, in emission order.
|
|
121
|
+
|
|
122
|
+
Can be a text block (user/assistant), thinking block (assistant),
|
|
123
|
+
tool_use block (assistant), or tool_result block (user).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
block_type: Literal["text", "thinking", "tool_use", "tool_result"] = Field(
|
|
127
|
+
description="Block kind: text, thinking, tool_use, or tool_result."
|
|
128
|
+
)
|
|
129
|
+
sequence: int = Field(description="0-indexed position within the parent message.content_blocks list.")
|
|
130
|
+
|
|
131
|
+
text: str | None = Field(default=None, description="Text content. Set when block_type='text' or 'tool_result'.")
|
|
132
|
+
|
|
133
|
+
thinking: str | None = Field(
|
|
134
|
+
default=None,
|
|
135
|
+
description="Extended-thinking reasoning content. Set when block_type='thinking'.",
|
|
136
|
+
)
|
|
137
|
+
signature: str | None = Field(default=None, description="Anthropic-signed signature for the thinking block.")
|
|
138
|
+
|
|
139
|
+
tool_use_id: str | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="Tool invocation ID; joins to CommandTelemetry. Set for tool_use or tool_result blocks.",
|
|
142
|
+
)
|
|
143
|
+
is_error: bool = Field(
|
|
144
|
+
default=False,
|
|
145
|
+
description="True if tool_result block represents an error.",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class UserMessage(BaseModel):
|
|
150
|
+
"""One user-side message (text or tool result) within a TurnRecord.
|
|
151
|
+
|
|
152
|
+
For simulator-driven dialog mode: captures user utterances from the
|
|
153
|
+
simulator plus orchestrator-side wall-clock timing. Populated in
|
|
154
|
+
simulation runs only.
|
|
155
|
+
|
|
156
|
+
For tool results: captured when a tool_result UserMessage arrives
|
|
157
|
+
from the SDK, containing tool result blocks and error status.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
role: Literal["user"] = "user"
|
|
161
|
+
|
|
162
|
+
text: str = Field(description="Utterance sent to the agent (stop token stripped).")
|
|
163
|
+
raw_text: str | None = Field(
|
|
164
|
+
default=None,
|
|
165
|
+
description="Simulator's raw output before stop-token stripping. None for pinned initial_prompt openers.",
|
|
166
|
+
)
|
|
167
|
+
stop_requested: bool | None = Field(
|
|
168
|
+
default=None, description="Whether the simulator emitted its stop token. None for pinned openers."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
started_at: datetime | None = Field(
|
|
172
|
+
default=None, description="Wall-clock start of the simulator LLM call. None for pinned openers."
|
|
173
|
+
)
|
|
174
|
+
completed_at: datetime | None = Field(
|
|
175
|
+
default=None, description="Wall-clock end of the simulator LLM call. None for pinned openers."
|
|
176
|
+
)
|
|
177
|
+
generation_duration_ms: float | None = Field(
|
|
178
|
+
default=None, description="completed_at - started_at in milliseconds. None for pinned openers."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
input_tokens: int = Field(default=0, description="Simulator prompt tokens (0 if SDK did not surface them).")
|
|
182
|
+
output_tokens: int = Field(default=0, description="Simulator completion tokens (0 if SDK did not surface them).")
|
|
183
|
+
model: str | None = Field(default=None, description="Simulator model identifier.")
|
|
184
|
+
simulator_failed: bool = Field(
|
|
185
|
+
default=False, description="True if the simulator call raised; text/raw_text reflect the fallback or are empty."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AssistantMessage(BaseModel):
|
|
190
|
+
"""One LLM response message (from SDK AssistantMessage) within a TurnRecord.
|
|
191
|
+
|
|
192
|
+
Captures per-message generation timing, content blocks, and token usage so
|
|
193
|
+
LLM time can be measured directly rather than inferred from gaps between
|
|
194
|
+
command timestamps. Per-message token usage is per-generation. The run's
|
|
195
|
+
authoritative cumulative billing comes from the SDK ResultMessage
|
|
196
|
+
``model_usage`` (cumulative per-model, reconciles to ``total_cost_usd``);
|
|
197
|
+
summing this stream is only a fallback when ``model_usage`` is absent (e.g.
|
|
198
|
+
a mock or non-conformant SDK). The stream under-reports sub-agent cache-creation/input — those
|
|
199
|
+
emissions are only partially bubbled up — so do NOT treat it as the source of
|
|
200
|
+
truth for run-level cost.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
role: Literal["assistant"] = "assistant"
|
|
204
|
+
|
|
205
|
+
started_at: datetime = Field(description="Wall-clock start of generation (end of the previous SDK event).")
|
|
206
|
+
completed_at: datetime = Field(description="Wall-clock arrival of the AssistantMessage from the SDK.")
|
|
207
|
+
generation_duration_ms: float = Field(
|
|
208
|
+
description="completed_at - started_at in milliseconds (wall-clock between SDK events).",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
content_blocks: list[ContentBlock] = Field(
|
|
212
|
+
default_factory=list,
|
|
213
|
+
description="Content blocks Claude emitted in this turn, in emission order.",
|
|
214
|
+
)
|
|
215
|
+
tool_use_ids: list[str] = Field(
|
|
216
|
+
default_factory=list,
|
|
217
|
+
description="tool_use_id values from content_blocks, for joining with CommandTelemetry and tool_result blocks.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
input_tokens: int = Field(
|
|
221
|
+
default=0,
|
|
222
|
+
description=(
|
|
223
|
+
"Input prompt tokens for this LLM call. Zero on follow-up emissions that "
|
|
224
|
+
"share a message_id with an earlier AssistantMessage (the CLI splits one "
|
|
225
|
+
"API call into multiple emissions); billing is recorded on the first only. "
|
|
226
|
+
"Per-call only; the run's cumulative input is taken from ResultMessage "
|
|
227
|
+
"model_usage (summing this stream is a fallback — it misses sub-agent input)."
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
output_tokens: int = Field(
|
|
231
|
+
default=0,
|
|
232
|
+
description=(
|
|
233
|
+
"Output tokens generated in this LLM call. Recovered from the "
|
|
234
|
+
"message_delta stream event when include_partial_messages is on; falls "
|
|
235
|
+
"back to the (often partial) SDK assistant-event value otherwise. Per-call "
|
|
236
|
+
"only; the run's cumulative output is taken from ResultMessage model_usage "
|
|
237
|
+
"(summing this stream is a fallback)."
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
cache_creation_tokens: int = Field(default=0, description="Tokens used to create prompt cache for this call.")
|
|
241
|
+
cache_read_tokens: int = Field(default=0, description="Tokens read from prompt cache for this call.")
|
|
242
|
+
reasoning_tokens: int = Field(default=0, description="Extended-thinking tokens; subset of output_tokens.")
|
|
243
|
+
|
|
244
|
+
stop_reason: str | None = Field(default=None, description="SDK stop reason: 'tool_use', 'end_turn', etc.")
|
|
245
|
+
model: str | None = Field(default=None, description="Model identifier that generated this turn.")
|
|
246
|
+
|
|
247
|
+
message_id: str | None = Field(
|
|
248
|
+
default=None,
|
|
249
|
+
description=(
|
|
250
|
+
"Anthropic API message_id. Multiple AssistantMessage records can share this id "
|
|
251
|
+
"when the Claude Code CLI splits one API response into per-block-kind events. "
|
|
252
|
+
"Downstream tooling can group by this id to recover one logical generation."
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
parent_tool_use_id: str | None = Field(
|
|
257
|
+
default=None,
|
|
258
|
+
description=(
|
|
259
|
+
"The Task tool_use_id that spawned this message's sub-agent, or null for the "
|
|
260
|
+
"main agent thread. Sub-agent (Task tool) calls bubble up into the same message "
|
|
261
|
+
"stream, but each sub-agent has its OWN context that is not re-read by the main "
|
|
262
|
+
"thread or sibling sub-agents — so the conversation is a tree, not a flat list. "
|
|
263
|
+
"Grouping by this id recovers the branch a call belongs to, which is required to "
|
|
264
|
+
"model the prompt-cache re-read cascade correctly (content cascades only within "
|
|
265
|
+
"its own branch). Sourced from the SDK AssistantMessage.parent_tool_use_id."
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ReconciliationMessage(BaseModel):
|
|
271
|
+
"""A synthetic transcript entry carrying tokens the agent billed but never
|
|
272
|
+
surfaced as a streamed generation.
|
|
273
|
+
|
|
274
|
+
The authoritative turn total (Claude's ``ResultMessage.model_usage``, Codex's
|
|
275
|
+
thread total + folded sub-agent tokens) is consistently LARGER than the sum
|
|
276
|
+
of the per-``AssistantMessage`` token buckets, for reasons that are
|
|
277
|
+
architectural, not bugs: a fixed prompt slice (~512 input tokens on Claude)
|
|
278
|
+
is billed but rides on no SDK-emitted message, and sub-agent input/cache is
|
|
279
|
+
only partially bubbled into the parent stream. Rather than fabricate
|
|
280
|
+
per-generation numbers (which would match no real call), the residual is
|
|
281
|
+
booked once, explicitly, as this entry — so that summing the transcript's
|
|
282
|
+
token buckets EXACTLY reproduces the authoritative ``token_usage`` for the
|
|
283
|
+
turn. Consumers that sum the stream (the evalboard) therefore reconcile to
|
|
284
|
+
the bill without a separate aggregate; this entry is the visible "missing
|
|
285
|
+
tokens" line. Carries no cost (cost stays on the authoritative aggregate) and
|
|
286
|
+
is never an LLM generation — it is excluded from generation/turn counts.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
role: Literal["reconciliation"] = "reconciliation"
|
|
290
|
+
|
|
291
|
+
input_tokens: int = Field(
|
|
292
|
+
default=0,
|
|
293
|
+
description="Residual uncached input tokens (authoritative turn total minus the per-message sum).",
|
|
294
|
+
)
|
|
295
|
+
output_tokens: int = Field(default=0, description="Residual output tokens.")
|
|
296
|
+
cache_creation_tokens: int = Field(default=0, description="Residual cache-creation tokens.")
|
|
297
|
+
cache_read_tokens: int = Field(default=0, description="Residual cache-read tokens.")
|
|
298
|
+
|
|
299
|
+
note: str = Field(
|
|
300
|
+
default="",
|
|
301
|
+
description="Human-readable explanation of why these tokens are unattributed (shown in the UI).",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class CommandTelemetry(BaseModel):
|
|
306
|
+
"""Telemetry for a single command execution.
|
|
307
|
+
|
|
308
|
+
Captures detailed information about each tool use by the agent,
|
|
309
|
+
enabling analysis, debugging, and optimization.
|
|
310
|
+
|
|
311
|
+
**Deprecation notice:** This model duplicates data already available in
|
|
312
|
+
TurnRecord.messages (as tool_use and tool_result blocks). In future versions,
|
|
313
|
+
command telemetry should be derived from the messages array rather than
|
|
314
|
+
maintained as a parallel structure. Kept for backward compatibility.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
# Identity
|
|
318
|
+
tool_name: str = Field(description="Tool name (Read, Write, Bash, Edit, Glob, etc.)")
|
|
319
|
+
tool_id: str = Field(description="Unique ID from Claude SDK for this tool invocation")
|
|
320
|
+
|
|
321
|
+
assistant_turn_index: int | None = Field(
|
|
322
|
+
default=None,
|
|
323
|
+
description=(
|
|
324
|
+
"0-indexed position among AssistantMessage entries only. "
|
|
325
|
+
"Filter turn_record.messages by role='assistant' before indexing."
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Timing: generation_completed_at is when Claude finished emitting the
|
|
330
|
+
# tool_use block; execution_* bracket the actual tool run. `timestamp`
|
|
331
|
+
# equals generation_completed_at and `duration_ms` equals
|
|
332
|
+
# execution_completed_at - execution_started_at; both are retained for
|
|
333
|
+
# downstream consumers that index on the original field names.
|
|
334
|
+
timestamp: datetime = Field(description="Equal to generation_completed_at; when the tool_use block arrived.")
|
|
335
|
+
duration_ms: float | None = Field(
|
|
336
|
+
default=None,
|
|
337
|
+
description="Command execution time in milliseconds (None = not complete, set in two-phase processing).",
|
|
338
|
+
)
|
|
339
|
+
generation_completed_at: datetime | None = Field(
|
|
340
|
+
default=None,
|
|
341
|
+
description="When Claude finished emitting the tool_use block.",
|
|
342
|
+
)
|
|
343
|
+
execution_started_at: datetime | None = Field(default=None, description="When tool execution began.")
|
|
344
|
+
execution_completed_at: datetime | None = Field(
|
|
345
|
+
default=None, description="When tool execution completed (success or error)."
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Parameters (structured)
|
|
349
|
+
parameters: dict[str, Any] = Field(
|
|
350
|
+
default_factory=dict, description="Structured command parameters (e.g., {'file_path': 'main.py'} for Read)"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Results
|
|
354
|
+
result_status: Literal["success", "error", "unknown"] | None = Field(
|
|
355
|
+
default=None,
|
|
356
|
+
description="Whether the command succeeded or failed (None = pending result, set during two-phase processing)",
|
|
357
|
+
)
|
|
358
|
+
result_summary: str | None = Field(
|
|
359
|
+
default=None,
|
|
360
|
+
description=(
|
|
361
|
+
"Full tool-result content as text (untruncated). Despite the name, this is "
|
|
362
|
+
"the complete result body, not a preview — kept whole so sub-agent returns "
|
|
363
|
+
"and other large payloads are preserved."
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
error_message: str | None = Field(default=None, description="Error message if command failed")
|
|
367
|
+
result_data: dict[str, Any] | list[Any] | None = Field(
|
|
368
|
+
default=None,
|
|
369
|
+
description=(
|
|
370
|
+
"First parsed JSON object or array found in the tool result content. "
|
|
371
|
+
"Prefix noise (e.g. CLI warning lines before the JSON body) and trailing "
|
|
372
|
+
"garbage are tolerated. None for content without any parseable JSON object "
|
|
373
|
+
"or array and for bare primitives (strings, numbers, booleans, null). "
|
|
374
|
+
"Complements result_summary (a short text preview) by preserving the full "
|
|
375
|
+
"structured payload so downstream consumers can extract specialised views "
|
|
376
|
+
"without re-parsing the log."
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Metadata
|
|
381
|
+
sequence_number: int = Field(default=0, description="Order within the turn (0-indexed)")
|
|
382
|
+
|
|
383
|
+
@computed_field # type: ignore[prop-decorator]
|
|
384
|
+
@property
|
|
385
|
+
def result_tokens(self) -> int:
|
|
386
|
+
"""Approximate token size of the tool result the model received.
|
|
387
|
+
|
|
388
|
+
Derived from the untruncated ``result_summary`` content (≈4 chars/token,
|
|
389
|
+
matching the evalboard heuristic), so it is a DIRECT, cache-independent
|
|
390
|
+
measure of "tool output size" — available identically whether prompt
|
|
391
|
+
caching was on or off. The cost simulator uses this instead of inferring
|
|
392
|
+
result size from prompt-cache growth (which is unavailable when caching is
|
|
393
|
+
disabled). Approximate, not the API's exact tokenizer count, but
|
|
394
|
+
deterministic and always present. 0 when the tool returned no content.
|
|
395
|
+
"""
|
|
396
|
+
if not self.result_summary:
|
|
397
|
+
return 0
|
|
398
|
+
return -(-len(self.result_summary) // 4) # ceil(len / 4)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
#: One entry in a turn's per-message transcript, discriminated on ``role``.
|
|
402
|
+
#: ``ReconciliationMessage`` is the synthetic "missing tokens" line that makes the
|
|
403
|
+
#: transcript's token buckets sum to the authoritative turn total. Defined once
|
|
404
|
+
#: here and reused by ``TurnRecord.messages`` and ``AgentEndEvent.messages`` so the
|
|
405
|
+
#: list element type is identical everywhere (avoids list-invariance friction).
|
|
406
|
+
TranscriptMessage = Annotated[UserMessage | AssistantMessage | ReconciliationMessage, Discriminator("role")]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SlowestCommandInfo(BaseModel):
|
|
410
|
+
"""Information about a slow command for performance analysis.
|
|
411
|
+
|
|
412
|
+
Type-safe model for reporting slowest commands in statistics.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
tool: str = Field(description="Tool name (e.g., 'Bash', 'Read')")
|
|
416
|
+
duration_ms: float = Field(description="Execution duration in milliseconds")
|
|
417
|
+
parameters: dict[str, Any] = Field(description="Command parameters")
|
|
418
|
+
tool_id: str | None = Field(default=None, description="Optional: Unique tool invocation ID")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class CommandStatistics(BaseModel):
|
|
422
|
+
"""Aggregated statistics for command usage in an evaluation.
|
|
423
|
+
|
|
424
|
+
Provides summary metrics for analysis and reporting.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
total_commands: int = Field(description="Total number of commands executed")
|
|
428
|
+
commands_by_tool: dict[str, int] = Field(
|
|
429
|
+
default_factory=dict, description="Count of commands per tool (e.g., {'Bash': 45, 'Read': 12})"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Timing
|
|
433
|
+
total_command_time_ms: float = Field(default=0.0, description="Total time spent executing commands (milliseconds)")
|
|
434
|
+
avg_command_time_ms: float | None = Field(default=None, description="Average command execution time")
|
|
435
|
+
slowest_commands: list[SlowestCommandInfo] = Field(
|
|
436
|
+
default_factory=list, description="Top 5 slowest commands with details (type-safe model)"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Success/Failure
|
|
440
|
+
successful_commands: int = Field(default=0, description="Commands that succeeded")
|
|
441
|
+
failed_commands: int = Field(default=0, description="Commands that failed")
|
|
442
|
+
unknown_commands: int = Field(
|
|
443
|
+
default=0,
|
|
444
|
+
description="Commands with unknown status (missing ResultMessage, indicates agent/SDK interruption)",
|
|
445
|
+
)
|
|
446
|
+
success_rate: float = Field(
|
|
447
|
+
default=0.0,
|
|
448
|
+
description="Percentage of known commands that succeeded: success / (success + failed), excludes unknown",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Patterns
|
|
452
|
+
most_common_sequence: str | None = Field(
|
|
453
|
+
default=None, description="Most common 3-command sequence (e.g., 'Read -> Edit -> Bash')"
|
|
454
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Template source models for sandbox initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StarterFile(BaseModel):
|
|
13
|
+
"""A file to create in the sandbox before agent starts."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(extra="forbid")
|
|
16
|
+
|
|
17
|
+
path: str = Field(description="Relative path in sandbox (e.g., 'src/main.py')")
|
|
18
|
+
content: str = Field(description="File content")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseTemplateSource(BaseModel, ABC):
|
|
22
|
+
"""Base class for template sources - defines the discriminated union.
|
|
23
|
+
|
|
24
|
+
Note: No type field here - each subclass defines its own Literal type.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="forbid")
|
|
28
|
+
|
|
29
|
+
def model_post_init(self, context: Any, /) -> None:
|
|
30
|
+
# Pin the discriminator tag into model_fields_set so it survives
|
|
31
|
+
# model_dump(exclude_unset=True) → model_validate() round-trips (the
|
|
32
|
+
# sandbox layer merge dumps with exclude_unset) even for
|
|
33
|
+
# directly-constructed sources, where the tag comes from the Literal
|
|
34
|
+
# default rather than the caller.
|
|
35
|
+
self.__pydantic_fields_set__.add("type")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TemplateDirSource(BaseTemplateSource):
|
|
39
|
+
"""Copy files from a local directory into the sandbox."""
|
|
40
|
+
|
|
41
|
+
type: Literal["template_dir"] = "template_dir"
|
|
42
|
+
path: str = Field(description="Path to template directory (relative to task YAML or absolute)")
|
|
43
|
+
mount_point: str = Field(
|
|
44
|
+
default=".",
|
|
45
|
+
description=(
|
|
46
|
+
"Destination subdirectory inside the sandbox where the template contents are copied. "
|
|
47
|
+
"Defaults to '.' (sandbox root). Must be a relative path that stays within the sandbox."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
include_patterns: list[str] = Field(
|
|
51
|
+
default_factory=list,
|
|
52
|
+
description=(
|
|
53
|
+
"Template-relative glob patterns to copy even when the path matches a default sandbox "
|
|
54
|
+
"ignore pattern. Author-controlled — re-including sensitive directories such as `.git` "
|
|
55
|
+
"or `.env` is possible, so review patterns when writing tasks. `*` does not stop at `/`, "
|
|
56
|
+
"see `_matches_template_include_pattern`."
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@field_validator("mount_point")
|
|
61
|
+
@classmethod
|
|
62
|
+
def _validate_mount_point(cls, v: str) -> str:
|
|
63
|
+
if not v:
|
|
64
|
+
raise ValueError("mount_point must not be empty")
|
|
65
|
+
if os.path.isabs(v) or v.startswith(("/", "\\")):
|
|
66
|
+
raise ValueError(f"mount_point must be a relative path, got: {v!r}")
|
|
67
|
+
parts = v.replace("\\", "/").split("/")
|
|
68
|
+
if any(p == ".." for p in parts):
|
|
69
|
+
raise ValueError(f"mount_point must not contain '..' segments, got: {v!r}")
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
@field_validator("include_patterns")
|
|
73
|
+
@classmethod
|
|
74
|
+
def _validate_include_patterns(cls, v: list[str]) -> list[str]:
|
|
75
|
+
for pattern in v:
|
|
76
|
+
if not pattern:
|
|
77
|
+
raise ValueError("include_patterns entries must not be empty")
|
|
78
|
+
if os.path.isabs(pattern) or pattern.startswith(("/", "\\")):
|
|
79
|
+
raise ValueError(f"include_patterns entries must be relative, got: {pattern!r}")
|
|
80
|
+
parts = pattern.replace("\\", "/").split("/")
|
|
81
|
+
if any(p == ".." for p in parts):
|
|
82
|
+
raise ValueError(f"include_patterns entries must not contain '..' segments, got: {pattern!r}")
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StarterFilesSource(BaseTemplateSource):
|
|
87
|
+
"""Create inline files from YAML definitions."""
|
|
88
|
+
|
|
89
|
+
type: Literal["starter_files"] = "starter_files"
|
|
90
|
+
files: list[StarterFile] = Field(description="List of files to create")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RepoSource(BaseTemplateSource):
|
|
94
|
+
"""Clone files from a git repository."""
|
|
95
|
+
|
|
96
|
+
type: Literal["repo"] = "repo"
|
|
97
|
+
url: str = Field(description="Git repository URL")
|
|
98
|
+
commit: str | None = Field(default=None, description="Specific commit SHA to checkout")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Discriminated union of template sources. The `type` tag is REQUIRED in
|
|
102
|
+
# dict/YAML input — a missing or unknown tag raises one crisp discriminator
|
|
103
|
+
# error instead of smart-union coercion. Per-variant Literal defaults remain
|
|
104
|
+
# for direct construction and serialization.
|
|
105
|
+
TemplateSource = Annotated[
|
|
106
|
+
TemplateDirSource | StarterFilesSource | RepoSource,
|
|
107
|
+
Field(discriminator="type"),
|
|
108
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Orchestration components for task evaluation lifecycle.
|
|
2
|
+
|
|
3
|
+
This package coordinates the complete evaluation flow from task loading
|
|
4
|
+
through sandbox setup, agent interaction, success checking, and cleanup.
|
|
5
|
+
|
|
6
|
+
Main components:
|
|
7
|
+
- config: Configuration models for batch execution
|
|
8
|
+
- task_loader: YAML task definition loading and validation
|
|
9
|
+
|
|
10
|
+
NO re-exports - use explicit imports:
|
|
11
|
+
from coder_eval.orchestration.config import BatchRunConfig
|
|
12
|
+
from coder_eval.orchestration.task_loader import load_task, resolve_template_paths
|
|
13
|
+
"""
|