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
coder_eval/__init__.py
ADDED
coder_eval/agent.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Abstract base class for coding agents."""
|
|
2
|
+
|
|
3
|
+
# by-design model-hub ↔ registry type-level cycle; runtime imports are lazy per CE017
|
|
4
|
+
# pyright: reportImportCycles=false
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, NoReturn, Protocol
|
|
9
|
+
|
|
10
|
+
from .errors import AgentCrashError, TurnTimeoutError
|
|
11
|
+
from .errors.agent import format_timeout_reason, truncate_crash_message
|
|
12
|
+
from .models import AgentState as AgentState
|
|
13
|
+
from .models import BaseAgentConfig, TurnRecord
|
|
14
|
+
from .streaming.callbacks import StreamCallback
|
|
15
|
+
from .streaming.collector import EventCollector
|
|
16
|
+
from .streaming.events import AgentEndStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _FinalizeFn(Protocol):
|
|
23
|
+
"""The per-turn ``finalize`` callback shared by every agent's turn-state.
|
|
24
|
+
|
|
25
|
+
Pinning the exact keyword-only signature here (instead of a loose
|
|
26
|
+
``Callable[..., None]``) lets pyright catch a future ``Agent`` subclass that
|
|
27
|
+
wires an incompatible ``finalize`` into the shared mid-turn failure kernels.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __call__(
|
|
31
|
+
self,
|
|
32
|
+
status: AgentEndStatus,
|
|
33
|
+
*,
|
|
34
|
+
crashed: bool = ...,
|
|
35
|
+
crash_reason: str | None = ...,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Finalize the current turn with the given end status."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Agent[ConfigT: BaseAgentConfig](ABC):
|
|
41
|
+
"""Abstract base class for all coding agent implementations.
|
|
42
|
+
|
|
43
|
+
Generic over ConfigT (the agent's config type) to enforce type-safe
|
|
44
|
+
configuration binding at the agent level. Concrete implementations
|
|
45
|
+
specify their config type:
|
|
46
|
+
|
|
47
|
+
class ClaudeCodeAgent(Agent[ClaudeCodeAgentConfig]):
|
|
48
|
+
def __init__(self, config: ClaudeCodeAgentConfig, ...):
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
This ensures mypy enforces the correct config type for each agent.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
pending_turn: TurnRecord | None = None
|
|
55
|
+
"""Side-channel for partial turn records from failed ``communicate()`` calls.
|
|
56
|
+
|
|
57
|
+
Implementations must set this to a ``crashed=True`` TurnRecord before
|
|
58
|
+
raising any mid-turn exception that carries captured telemetry. Callers
|
|
59
|
+
must read this slot after every failed ``communicate()`` call, then call
|
|
60
|
+
``discard_pending_turn()`` to clear it. Outside ``communicate()``, this
|
|
61
|
+
slot is always None.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Shared turn-lifecycle bookkeeping. Class-level defaults so subclasses get
|
|
65
|
+
# the behavior without re-declaring them in __init__ (they may still set
|
|
66
|
+
# `_state` in start()). `_iteration_was_incremented` is set True right after
|
|
67
|
+
# the counter bump at the top of `communicate()` and consumed by
|
|
68
|
+
# `discard_pending_turn()`, which rolls the counter back exactly once per
|
|
69
|
+
# failed turn — even when partial-record assembly leaves `pending_turn=None`.
|
|
70
|
+
_state: AgentState = AgentState.WORKING
|
|
71
|
+
_iteration: int = 0
|
|
72
|
+
_iteration_was_incremented: bool = False
|
|
73
|
+
|
|
74
|
+
def _begin_turn(self) -> None:
|
|
75
|
+
"""Mark the start of a ``communicate()`` turn: reset the pending slot and
|
|
76
|
+
bump the iteration counter so a mid-turn failure can be rolled back.
|
|
77
|
+
|
|
78
|
+
Call once at the top of every ``communicate()`` implementation.
|
|
79
|
+
"""
|
|
80
|
+
self.pending_turn = None
|
|
81
|
+
self._iteration += 1
|
|
82
|
+
self._iteration_was_incremented = True
|
|
83
|
+
|
|
84
|
+
def _end_turn_ok(self) -> None:
|
|
85
|
+
"""Mark a turn as cleanly completed so its iteration bump stands.
|
|
86
|
+
|
|
87
|
+
Call on the success path of ``communicate()`` (before returning).
|
|
88
|
+
"""
|
|
89
|
+
self._iteration_was_incremented = False
|
|
90
|
+
|
|
91
|
+
def _mark_stopped(self) -> None:
|
|
92
|
+
"""Common ``stop()`` tail: clear the pending slot and enter FINISHED.
|
|
93
|
+
|
|
94
|
+
Subclasses call this after their own resource teardown.
|
|
95
|
+
"""
|
|
96
|
+
self.pending_turn = None
|
|
97
|
+
self._state = AgentState.FINISHED
|
|
98
|
+
|
|
99
|
+
# --- Shared mid-turn failure kernels --------------------------------------
|
|
100
|
+
#
|
|
101
|
+
# Tiny, byte-identical fragments that recur across (and within) the agent
|
|
102
|
+
# turn-loops. Each agent keeps its OWN outer try/except/finally bracket — the
|
|
103
|
+
# brackets genuinely differ (flat vs nested, finally vs not) — and calls these
|
|
104
|
+
# from inside its existing branches. They take the agent's own per-turn
|
|
105
|
+
# ``finalize`` callable (the turn-state's method) so the helper never needs to
|
|
106
|
+
# know how each agent assembles its AgentEndEvent payload.
|
|
107
|
+
|
|
108
|
+
def _finalize_and_raise_timeout(
|
|
109
|
+
self, finalize: _FinalizeFn, timeout: float, *, cause: BaseException | None = None
|
|
110
|
+
) -> NoReturn:
|
|
111
|
+
"""Mark ERROR, finalize the turn as a timed-out crash, raise TurnTimeoutError.
|
|
112
|
+
|
|
113
|
+
Reproduces the per-branch ``_state=ERROR -> finalize(TIMEOUT) -> raise`` triple
|
|
114
|
+
that appears three times in Claude plus once in Codex. When called from inside
|
|
115
|
+
an ``except ... as e`` block, pass ``cause=e`` to preserve the explicit
|
|
116
|
+
``__cause__`` link; otherwise Python's implicit ``__context__`` chaining stands.
|
|
117
|
+
"""
|
|
118
|
+
self._state = AgentState.ERROR
|
|
119
|
+
finalize(AgentEndStatus.TIMEOUT, crashed=True, crash_reason=format_timeout_reason(timeout))
|
|
120
|
+
if cause is not None:
|
|
121
|
+
raise TurnTimeoutError(timeout, iteration=self._iteration) from cause
|
|
122
|
+
raise TurnTimeoutError(timeout, iteration=self._iteration)
|
|
123
|
+
|
|
124
|
+
def _finalize_and_raise_crash(
|
|
125
|
+
self, finalize: _FinalizeFn, message: str, *, cause: BaseException | None = None
|
|
126
|
+
) -> NoReturn:
|
|
127
|
+
"""Mark ERROR, finalize the turn as a crash, raise AgentCrashError.
|
|
128
|
+
|
|
129
|
+
``message`` is the agent-built error string (the helper does NOT construct
|
|
130
|
+
it). ``crash_reason`` is truncated for storage while the raised
|
|
131
|
+
``AgentCrashError`` carries ``message`` as passed (truncation is idempotent,
|
|
132
|
+
so an already-truncated message round-trips unchanged). When called from
|
|
133
|
+
inside an ``except ... as e`` block, pass ``cause=e`` to preserve the explicit
|
|
134
|
+
``__cause__`` link; otherwise Python's implicit ``__context__`` chaining stands.
|
|
135
|
+
"""
|
|
136
|
+
self._state = AgentState.ERROR
|
|
137
|
+
finalize(AgentEndStatus.CRASHED, crashed=True, crash_reason=truncate_crash_message(message))
|
|
138
|
+
if cause is not None:
|
|
139
|
+
raise AgentCrashError(message) from cause
|
|
140
|
+
raise AgentCrashError(message)
|
|
141
|
+
|
|
142
|
+
def _capture_partial_turn(self, collector: EventCollector) -> None:
|
|
143
|
+
"""Build the crashed partial ``TurnRecord`` into ``pending_turn`` (best-effort).
|
|
144
|
+
|
|
145
|
+
Shared crash-tail of each agent's ``finalize``: if assembling the partial
|
|
146
|
+
record itself raises, swallow it and leave ``pending_turn`` None rather than
|
|
147
|
+
masking the original mid-turn failure.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
self.pending_turn = collector.build_turn_record()
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.exception("Failed to build partial turn record")
|
|
153
|
+
self.pending_turn = None
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
async def start(
|
|
157
|
+
self,
|
|
158
|
+
working_directory: str,
|
|
159
|
+
*,
|
|
160
|
+
env_path_prepend: list[str] | None = None,
|
|
161
|
+
plugin_tools_dir: str | None = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Initialize and start the agent.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
working_directory: Path to the working directory for the agent
|
|
167
|
+
env_path_prepend: Optional absolute directories to prepend to PATH for any
|
|
168
|
+
subprocess the agent spawns (typically resolved sandbox mock dirs).
|
|
169
|
+
Implementations that don't shell out may ignore this argument.
|
|
170
|
+
plugin_tools_dir: Optional canonical ``node_modules/@uipath`` to export as
|
|
171
|
+
``PLUGIN_TOOLS_DIR`` so the agent's UiPath CLI pins plugin discovery
|
|
172
|
+
instead of walking up from CWD. An external ``PLUGIN_TOOLS_DIR`` in
|
|
173
|
+
the process environment still wins. Implementations that don't shell
|
|
174
|
+
out may ignore this argument.
|
|
175
|
+
"""
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
async def communicate(
|
|
180
|
+
self,
|
|
181
|
+
user_input: str,
|
|
182
|
+
*,
|
|
183
|
+
stream_callback: StreamCallback | None = None,
|
|
184
|
+
timeout: float | None = None,
|
|
185
|
+
max_turns: int | None = None,
|
|
186
|
+
) -> TurnRecord:
|
|
187
|
+
"""Send a message to the agent and receive its response.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
user_input: The message/prompt to send to the agent
|
|
191
|
+
stream_callback: Optional callback for real-time event streaming
|
|
192
|
+
timeout: Hard wall-clock deadline in seconds. When exceeded the
|
|
193
|
+
agent must force-terminate any in-flight subprocess and raise
|
|
194
|
+
TurnTimeoutError. Implementations should not rely solely on
|
|
195
|
+
asyncio cancellation (the Claude Agent SDK uses anyio task
|
|
196
|
+
groups that swallow cooperative cancellation).
|
|
197
|
+
max_turns: Hard cap on inner-loop turns within this single
|
|
198
|
+
``communicate()`` call. When the agent would exceed it, the
|
|
199
|
+
returned ``TurnRecord`` has ``max_turns_exhausted=True``.
|
|
200
|
+
None defers to the underlying SDK default.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
TurnRecord containing the complete interaction
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
RuntimeError: If agent is not started or communication fails.
|
|
207
|
+
TurnTimeoutError: Timeout elapsed; implementations must set
|
|
208
|
+
``self.pending_turn`` to a ``crashed=True`` partial TurnRecord
|
|
209
|
+
before raising if telemetry was captured.
|
|
210
|
+
AgentCrashError: Agent failed mid-turn; same ``pending_turn`` contract.
|
|
211
|
+
|
|
212
|
+
On success, ``pending_turn`` must be None and the completed TurnRecord
|
|
213
|
+
is returned directly. On failure, ``pending_turn`` is set (if telemetry
|
|
214
|
+
was available) before raising — rollback of per-turn bookkeeping happens
|
|
215
|
+
exclusively in ``discard_pending_turn``, which the caller invokes after
|
|
216
|
+
every failed ``communicate()``.
|
|
217
|
+
|
|
218
|
+
Streaming contract: the agent is the SOLE emitter of the standardized
|
|
219
|
+
event protocol (the orchestrator is a pure consumer). An implementation
|
|
220
|
+
MUST emit exactly one ``AgentStartEvent`` at the top of ``communicate()``
|
|
221
|
+
and exactly one matching ``AgentEndEvent`` on every exit path (success,
|
|
222
|
+
crash, or timeout — emit it from ``finally``), with one ``TurnStartEvent``
|
|
223
|
+
/ ``TurnEndEvent`` pair per inner turn and ``ToolStartEvent`` /
|
|
224
|
+
``ToolEndEvent`` for each tool call (every ``ToolStart`` closed by a
|
|
225
|
+
``ToolEnd``, including ``status=unresolved`` for tools orphaned by a crash).
|
|
226
|
+
Events fan out through an internal ``EventCollector`` (which builds the
|
|
227
|
+
returned ``TurnRecord``) and the caller's ``stream_callback``; renderers
|
|
228
|
+
and the task-log handler consume the same stream.
|
|
229
|
+
"""
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
@abstractmethod
|
|
233
|
+
async def stop(self) -> None:
|
|
234
|
+
"""Stop the agent and clean up resources."""
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
async def kill(self) -> None:
|
|
238
|
+
"""Force-terminate any in-flight subprocess started by this agent.
|
|
239
|
+
|
|
240
|
+
Safe to call at any time, including when no subprocess is active.
|
|
241
|
+
Used by the orchestrator to escape SDKs that ignore cooperative
|
|
242
|
+
cancellation. Default implementation is a no-op.
|
|
243
|
+
"""
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
async def discard_pending_turn(self) -> None:
|
|
247
|
+
"""Clear ``pending_turn`` and roll back the iteration counter.
|
|
248
|
+
|
|
249
|
+
Rolls back when either signal says a turn was attempted: the
|
|
250
|
+
``_iteration_was_incremented`` flag (survives partial-record assembly
|
|
251
|
+
swallowing an exception, which leaves ``pending_turn=None`` — so the
|
|
252
|
+
flag, not ``pending_turn``, is the reliable signal) or a non-None
|
|
253
|
+
``pending_turn`` (for callers, e.g. tests, that set it directly).
|
|
254
|
+
|
|
255
|
+
Idempotent: after the first call both signals are cleared. Call only
|
|
256
|
+
after a failed ``communicate()``; never after a success.
|
|
257
|
+
"""
|
|
258
|
+
should_rollback = self._iteration_was_incremented or self.pending_turn is not None
|
|
259
|
+
self.pending_turn = None
|
|
260
|
+
self._iteration_was_incremented = False
|
|
261
|
+
if should_rollback and self._iteration > 0:
|
|
262
|
+
self._iteration -= 1
|
|
263
|
+
|
|
264
|
+
def kill_sync(self) -> None:
|
|
265
|
+
"""Synchronous variant of ``kill`` for callers on non-asyncio threads.
|
|
266
|
+
|
|
267
|
+
Invoked by ``ThreadedWatchdog`` from its timer thread, which cannot
|
|
268
|
+
await coroutines. Safe to call at any time. Default implementation
|
|
269
|
+
is a no-op; concrete agents override to SIGKILL any in-flight
|
|
270
|
+
subprocess by PID.
|
|
271
|
+
"""
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
def get_state(self) -> AgentState:
|
|
275
|
+
"""Get the current state of the agent.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Current agent state
|
|
279
|
+
"""
|
|
280
|
+
return self._state
|
|
281
|
+
|
|
282
|
+
def get_sdk_options(self) -> dict[str, Any] | None:
|
|
283
|
+
"""Get the raw SDK options used for the last agent query.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dictionary of SDK option field names to values, or None if not available.
|
|
287
|
+
"""
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def get_environment_info(self) -> dict[str, Any]:
|
|
291
|
+
"""Agent-specific routing/environment details to persist into the run's
|
|
292
|
+
``EvaluationResult.environment_info``.
|
|
293
|
+
|
|
294
|
+
Lets an agent surface non-default endpoint/model routing (e.g. a custom
|
|
295
|
+
base URL or wire protocol) so runs are auditable and comparable across
|
|
296
|
+
operators. The orchestrator merges this into ``environment_info`` after
|
|
297
|
+
the agent starts. Default: nothing to add.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
A flat dict of JSON-serializable keys to merge; empty by default.
|
|
301
|
+
"""
|
|
302
|
+
return {}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Agent implementations and registry."""
|
|
2
|
+
|
|
3
|
+
# Import agents to trigger their @register decorators
|
|
4
|
+
from coder_eval.agents.antigravity_agent import AntigravityAgent
|
|
5
|
+
from coder_eval.agents.claude_code_agent import ClaudeCodeAgent
|
|
6
|
+
from coder_eval.agents.codex_agent import CodexAgent
|
|
7
|
+
from coder_eval.agents.noop_agent import NoOpAgent
|
|
8
|
+
from coder_eval.agents.registry import AgentRegistry, create_agent
|
|
9
|
+
from coder_eval.models import AgentKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_builtins(registry: type[AgentRegistry]) -> None:
|
|
13
|
+
"""Register the built-in agents (Claude/Codex/Antigravity/NoOp) onto ``registry``.
|
|
14
|
+
|
|
15
|
+
This is the target of coder-eval's own ``coder_eval.plugins`` entry point, so
|
|
16
|
+
the built-in agents travel the identical discovery path as any third-party
|
|
17
|
+
plugin (see :mod:`coder_eval.plugins`). Importing the agent modules above
|
|
18
|
+
fires their ``@AgentRegistry.register`` decorators; this hook only has to
|
|
19
|
+
ensure those modules are imported, which the package import already did.
|
|
20
|
+
"""
|
|
21
|
+
# Reference the imported classes so the registration side effect is explicit
|
|
22
|
+
# and a future refactor that drops the top-level imports fails loudly here.
|
|
23
|
+
_ = (ClaudeCodeAgent, CodexAgent, AntigravityAgent, NoOpAgent)
|
|
24
|
+
# Rot-protection: the decorators fire on import, but assert the built-ins are
|
|
25
|
+
# actually registered so a future lazy-import refactor (which would leave the
|
|
26
|
+
# import-cached modules' decorators un-run) fails loudly instead of silently
|
|
27
|
+
# registering nothing.
|
|
28
|
+
for kind in (AgentKind.CLAUDE_CODE, AgentKind.CODEX, AgentKind.ANTIGRAVITY, AgentKind.NONE):
|
|
29
|
+
if registry.get(kind) is None:
|
|
30
|
+
raise RuntimeError(f"register_builtins: built-in agent kind {kind!r} did not register")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AgentRegistry",
|
|
35
|
+
"AntigravityAgent",
|
|
36
|
+
"ClaudeCodeAgent",
|
|
37
|
+
"CodexAgent",
|
|
38
|
+
"NoOpAgent",
|
|
39
|
+
"create_agent",
|
|
40
|
+
"register_builtins",
|
|
41
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Shared logging helpers for agent implementations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrefixedAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
|
|
9
|
+
"""LoggerAdapter that prefixes every record with an ``[instance]`` tag.
|
|
10
|
+
|
|
11
|
+
Used to distinguish simultaneous agents in the same run — e.g. ``[coder]``
|
|
12
|
+
for the coding agent and ``[simulator]`` for the tools-disabled
|
|
13
|
+
user-simulator agent — without spinning up a separate logger hierarchy per
|
|
14
|
+
instance.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def process(self, msg, kwargs): # type: ignore[override]
|
|
18
|
+
return f"[{self.extra['prefix']}] {msg}", kwargs # type: ignore[index]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_RAW_SDK_LOG_ENV = "CODER_EVAL_RAW_SDK_LOG"
|
|
22
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def raw_sdk_logging_enabled() -> bool:
|
|
26
|
+
"""Whether verbatim SDK-event dumping is opted in via ``CODER_EVAL_RAW_SDK_LOG``."""
|
|
27
|
+
return os.environ.get(_RAW_SDK_LOG_ENV, "").strip().lower() in _TRUTHY
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def log_raw_sdk_event(
|
|
31
|
+
log: logging.LoggerAdapter, # type: ignore[type-arg]
|
|
32
|
+
*,
|
|
33
|
+
repr_target: Any,
|
|
34
|
+
attr_target: Any | None = None,
|
|
35
|
+
**header_fields: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Dump an SDK event verbatim, the instant it arrives, when opted in.
|
|
38
|
+
|
|
39
|
+
Gated behind ``CODER_EVAL_RAW_SDK_LOG`` so normal runs stay quiet. Emits at
|
|
40
|
+
INFO so it shows up in task.log without flipping the whole logger to DEBUG.
|
|
41
|
+
Shared by every agent so the dump format is identical across backends.
|
|
42
|
+
|
|
43
|
+
For each event we log, in order:
|
|
44
|
+
* the caller-supplied ``header_fields`` (e.g. ``type=`` for Claude,
|
|
45
|
+
``method=``/``root_type=`` for Codex),
|
|
46
|
+
* the full ``repr(repr_target)`` (untruncated), and
|
|
47
|
+
* a sorted ``key=value`` dump of every public attribute of
|
|
48
|
+
``attr_target`` (defaulting to ``repr_target``), so token fields like
|
|
49
|
+
``usage`` / ``model_usage`` are visible exactly as the SDK delivered
|
|
50
|
+
them even when ``repr`` is terse.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
log: The agent's prefixed logger adapter.
|
|
54
|
+
repr_target: The object to ``repr()`` in full.
|
|
55
|
+
attr_target: The object to introspect for the attribute dump. Defaults
|
|
56
|
+
to ``repr_target`` (Codex passes the notification's item root here,
|
|
57
|
+
falling back to the notification when the root is absent).
|
|
58
|
+
header_fields: Arbitrary ``key=value`` pairs rendered into the header
|
|
59
|
+
line, in insertion order.
|
|
60
|
+
"""
|
|
61
|
+
if not raw_sdk_logging_enabled():
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
raw_repr = repr(repr_target)
|
|
66
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
67
|
+
raw_repr = f"<unreprable: {exc!r}>"
|
|
68
|
+
|
|
69
|
+
target = repr_target if attr_target is None else attr_target
|
|
70
|
+
attrs: dict[str, Any] = {}
|
|
71
|
+
for name in dir(target):
|
|
72
|
+
if name.startswith("_"):
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
value = getattr(target, name)
|
|
76
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
77
|
+
value = f"<unreadable: {exc!r}>"
|
|
78
|
+
if callable(value):
|
|
79
|
+
continue
|
|
80
|
+
attrs[name] = value
|
|
81
|
+
attr_dump = "\n".join(f" {k} = {v!r}" for k, v in sorted(attrs.items()))
|
|
82
|
+
|
|
83
|
+
header = " ".join(f"{k}={v}" for k, v in header_fields.items())
|
|
84
|
+
log.info(
|
|
85
|
+
"RAW_SDK_EVENT %s\n repr=%s\n attrs:\n%s",
|
|
86
|
+
header,
|
|
87
|
+
raw_repr,
|
|
88
|
+
attr_dump or " (none)",
|
|
89
|
+
)
|