coder-eval 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,2 @@
1
+ # treat Markdown as text so GitHub renders it
2
+ *.md text linguist-language=Markdown
coder_eval/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """coder_eval - A framework for evaluating AI coding agents."""
2
+
3
+ __version__ = "0.8.2"
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
+ )