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,213 @@
1
+ """Typed verdict tool channel — replaces text-with-JSON parsing for judge criteria.
2
+
3
+ Defines a single ``submit_verdict`` tool the judge calls as its terminal action,
4
+ exposed in two native shapes (SDK MCP, Anthropic) so each judge backend can use
5
+ its own tool-calling protocol. The extractors then collapse those native
6
+ responses into a single ``tuple[JudgeVerdict | None, str | None]`` contract so
7
+ the rest of the criterion code doesn't branch on backend.
8
+
9
+ The shared anchor is ``JudgeVerdict.model_validate(args_dict)``: every backend
10
+ ultimately delivers a ``dict``-shaped tool input, and that one call decides
11
+ validity. Error vocabulary is preserved via ``_format_validation_error`` so
12
+ existing tests / on-disk records keep matching.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ from claude_agent_sdk import SdkMcpTool, create_sdk_mcp_server, tool
21
+ from claude_agent_sdk.types import McpSdkServerConfig
22
+ from pydantic import ValidationError
23
+
24
+ from coder_eval.models import JudgeVerdict
25
+
26
+
27
+ SUBMIT_VERDICT_TOOL_NAME = "submit_verdict"
28
+ """Single source of truth for the tool name across all three backends."""
29
+
30
+ SUBMIT_VERDICT_MCP_TOOL_NAME = "mcp__coder_eval_judge__submit_verdict"
31
+ """SDK ``allowed_tools`` entry — must match the SDK's ``mcp__<server>__<tool>`` convention."""
32
+
33
+ _SUBMIT_VERDICT_DESCRIPTION = (
34
+ "Submit your final score, rationale, and findings. Call exactly once as your terminal action."
35
+ )
36
+
37
+
38
+ SUBMIT_VERDICT_ANTHROPIC_TOOL: dict[str, Any] = {
39
+ "name": SUBMIT_VERDICT_TOOL_NAME,
40
+ "description": _SUBMIT_VERDICT_DESCRIPTION,
41
+ "input_schema": JudgeVerdict.model_json_schema(),
42
+ }
43
+ """Anthropic-native tool spec for the Bedrock httpx-direct path and Anthropic SDK calls.
44
+
45
+ Note: Anthropic uses ``input_schema``, not OpenAI's ``parameters``.
46
+ """
47
+
48
+
49
+ @dataclass
50
+ class VerdictCapture:
51
+ """Closure-side state shared between the SDK ``@tool`` and the criterion caller.
52
+
53
+ The SDK tool runs inside the agent's MCP server but writes through to this
54
+ plain object so the criterion can read the verdict after the agent loop
55
+ finishes. Each judge invocation owns a fresh ``VerdictCapture`` — no global
56
+ state, safe under concurrent judge runs.
57
+ """
58
+
59
+ verdict: JudgeVerdict | None = None
60
+ error: str | None = None
61
+ called_count: int = 0
62
+ # LAST observed call wins: a successful retry overwrites the previous verdict,
63
+ # and an invalid retry clears the prior valid verdict (and sets ``error``).
64
+ # The criterion reads the final state via ``extract_verdict_from_capture``.
65
+
66
+
67
+ def _build_submit_verdict_tool(capture: VerdictCapture) -> SdkMcpTool[Any]:
68
+ """Construct the ``@tool``-decorated submit_verdict closure bound to ``capture``.
69
+
70
+ Kept as a separate helper so tests can drive the underlying handler directly
71
+ (``SdkMcpTool.handler``) without going through the MCP Server's JSONRPC layer.
72
+ """
73
+
74
+ @tool(
75
+ SUBMIT_VERDICT_TOOL_NAME,
76
+ _SUBMIT_VERDICT_DESCRIPTION,
77
+ JudgeVerdict.model_json_schema(),
78
+ )
79
+ async def submit_verdict(args: dict[str, Any]) -> dict[str, Any]:
80
+ capture.called_count += 1
81
+ try:
82
+ verdict = JudgeVerdict.model_validate(args)
83
+ except ValidationError as e:
84
+ # LAST-call discipline: overwrite verdict too — a successful first
85
+ # call followed by an invalid retry should surface the latest state.
86
+ capture.verdict = None
87
+ capture.error = _format_validation_error(e)
88
+ return {"content": [{"type": "text", "text": f"Invalid: {capture.error}. Retry."}]}
89
+ capture.verdict = verdict
90
+ capture.error = None
91
+ return {"content": [{"type": "text", "text": "Verdict received. Stop here."}]}
92
+
93
+ return submit_verdict
94
+
95
+
96
+ def build_submit_verdict_mcp_server(capture: VerdictCapture) -> tuple[str, McpSdkServerConfig]:
97
+ """Build the ``submit_verdict`` SDK MCP server bound to ``capture``.
98
+
99
+ Returns ``(server_name, McpSdkServerConfig)`` ready to plug into
100
+ ``ClaudeAgentOptions.mcp_servers``. The server name is
101
+ ``"coder_eval_judge"`` and the resulting allowed-tool entry is
102
+ ``SUBMIT_VERDICT_MCP_TOOL_NAME`` (the SDK exposes MCP tools as
103
+ ``mcp__<server>__<tool>``).
104
+ """
105
+ sdk_tool = _build_submit_verdict_tool(capture)
106
+ server = create_sdk_mcp_server("coder_eval_judge", "1.0.0", [sdk_tool])
107
+ return "coder_eval_judge", server
108
+
109
+
110
+ def extract_verdict_from_capture(
111
+ capture: VerdictCapture,
112
+ ) -> tuple[JudgeVerdict | None, str | None]:
113
+ """Read the LAST successful verdict from ``capture``.
114
+
115
+ Returns ``(verdict, None)`` on success, ``(None, error)`` when the tool
116
+ was called with invalid args, and ``(None, "Judge did not call submit_verdict")``
117
+ when the tool was never called at all.
118
+ """
119
+ if capture.verdict is not None:
120
+ return capture.verdict, None
121
+ if capture.error is not None:
122
+ return None, capture.error
123
+ return None, "Judge did not call submit_verdict"
124
+
125
+
126
+ def extract_verdict_from_anthropic_response(
127
+ response: dict[str, Any],
128
+ ) -> tuple[JudgeVerdict | None, str | None]:
129
+ """Read the LAST ``submit_verdict`` tool_use block from an Anthropic-shaped response.
130
+
131
+ Walks ``response["content"]`` for blocks matching
132
+ ``{"type": "tool_use", "name": "submit_verdict"}`` and validates the last
133
+ one's ``input`` against ``JudgeVerdict``. Used by:
134
+
135
+ * The Bedrock httpx path (``invoke_bedrock_judge``) — raw JSON dict.
136
+ * The Anthropic SDK Direct path (``invoke_anthropic_judge``) —
137
+ response converted via ``Message.model_dump()``.
138
+
139
+ Both backends share Anthropic's native message shape.
140
+
141
+ LAST-call discipline + non-dict guard: a final ``submit_verdict`` block
142
+ whose ``input`` is missing or not a dict is treated as an invalid-args
143
+ failure, not "did not call".
144
+ """
145
+ blocks = response.get("content") or []
146
+ saw_submit_verdict = False
147
+ last_input: dict[str, Any] | None = None
148
+ for block in blocks:
149
+ if not isinstance(block, dict):
150
+ continue
151
+ if block.get("type") != "tool_use" or block.get("name") != SUBMIT_VERDICT_TOOL_NAME:
152
+ continue
153
+ saw_submit_verdict = True
154
+ block_input = block.get("input")
155
+ last_input = block_input if isinstance(block_input, dict) else None
156
+ if not saw_submit_verdict:
157
+ return None, "Judge did not call submit_verdict"
158
+ if last_input is None:
159
+ return None, "submit_verdict input must be an object"
160
+ try:
161
+ return JudgeVerdict.model_validate(last_input), None
162
+ except ValidationError as e:
163
+ return None, _format_validation_error(e)
164
+
165
+
166
+ def _format_validation_error(e: ValidationError) -> str:
167
+ """Normalize a Pydantic ``ValidationError`` into the legacy vocabulary.
168
+
169
+ Preserves the strings produced by the old ``_verdict_shaped_error`` so
170
+ persisted ``task.json`` records and existing test assertions stay stable:
171
+ * ``score field missing in judge verdict``
172
+ * ``score field is not a number: <repr>``
173
+ * ``score field is not a finite number: <repr>``
174
+ * ``rationale field must be a string, got <type>``
175
+ * ``rationale field is empty after whitespace collapse``
176
+
177
+ Multiple errors join with ``"; "``.
178
+ """
179
+ messages: list[str] = []
180
+ for err in e.errors():
181
+ loc = err.get("loc", ())
182
+ field = loc[0] if loc else ""
183
+ etype = err.get("type", "")
184
+ msg = err.get("msg", "")
185
+
186
+ if etype == "missing":
187
+ # Generic on ``field`` so any future required ``JudgeVerdict`` field gets
188
+ # the legacy "<name> field missing in judge verdict" vocabulary. Today
189
+ # ``score`` is the only required field; the test suite pins the string.
190
+ messages.append(f"{field or 'unknown'} field missing in judge verdict")
191
+ continue
192
+
193
+ # Pydantic wraps custom ValueErrors as "Value error, <body>".
194
+ body = msg[len("Value error, ") :] if msg.startswith("Value error, ") else msg
195
+
196
+ if field == "score" and body.startswith("score is not a number:"):
197
+ messages.append("score field is not a number:" + body[len("score is not a number:") :])
198
+ continue
199
+ if field == "score" and body.startswith("score is not finite:"):
200
+ messages.append("score field is not a finite number:" + body[len("score is not finite:") :])
201
+ continue
202
+ if field == "rationale" and body.startswith("rationale must be a string,"):
203
+ messages.append("rationale field must be a string," + body[len("rationale must be a string,") :])
204
+ continue
205
+ if field == "rationale" and body == "rationale is empty after whitespace collapse":
206
+ messages.append("rationale field is empty after whitespace collapse")
207
+ continue
208
+
209
+ # Fallback for any new error class we haven't mapped — surface the field qualifier
210
+ # so the diagnostic still names the offending key.
211
+ messages.append(f"{field} field: {body}" if field else body)
212
+
213
+ return "; ".join(messages)
@@ -0,0 +1,191 @@
1
+ """Pretty-print SDK payloads for display in logs and stream events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any
8
+
9
+ from claude_agent_sdk import (
10
+ AssistantMessage,
11
+ Message,
12
+ ResultMessage,
13
+ SystemMessage,
14
+ UserMessage,
15
+ )
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def format_messages(
22
+ messages: list[Message],
23
+ *,
24
+ warned_unknown_types: set[str],
25
+ log: logging.Logger | logging.LoggerAdapter[Any] = logger,
26
+ ) -> str:
27
+ """Render a list of SDK messages as a readable transcript string.
28
+
29
+ System and user messages are filtered out; assistant text, result status,
30
+ and ``tool_use`` stream events are tagged. Unknown message types emit only a
31
+ bare ``[TypeName]`` tag — never a truncated ``__repr__``, which could leak
32
+ unmatched braces into the transcript persisted to ``task.json``.
33
+
34
+ Uses ``isinstance`` (not type-name equality) so ``SystemMessage`` subclasses
35
+ (e.g. ``TaskStartedMessage``) hit the ``SystemMessage`` arm per their drop-in
36
+ contract.
37
+
38
+ Args:
39
+ messages: SDK message objects from a turn.
40
+ warned_unknown_types: Mutable set used to deduplicate the
41
+ "unhandled SDK message type" warning across calls; the caller owns
42
+ it (e.g. one per agent instance) so the warning fires once per type.
43
+ log: Logger (or adapter) for that warning; defaults to this module's logger.
44
+
45
+ Returns:
46
+ The formatted transcript, or ``"[No output]"`` when nothing was emitted.
47
+ """
48
+ formatted_parts = []
49
+
50
+ for msg in messages:
51
+ if isinstance(msg, SystemMessage):
52
+ continue
53
+
54
+ if isinstance(msg, UserMessage):
55
+ continue
56
+
57
+ if isinstance(msg, AssistantMessage):
58
+ content = getattr(msg, "content", "")
59
+ if isinstance(content, list):
60
+ for block in content:
61
+ text = getattr(block, "text", None)
62
+ if text:
63
+ formatted_parts.append(f"[ASSISTANT] {text}")
64
+ elif content:
65
+ formatted_parts.append(f"[ASSISTANT] {content}")
66
+ continue
67
+
68
+ if isinstance(msg, ResultMessage):
69
+ result_text = getattr(msg, "result", "") or ""
70
+ is_error = getattr(msg, "is_error", False)
71
+ status = "ERROR" if is_error else "SUCCESS"
72
+ formatted_parts.append(f"[RESULT - {status}] {result_text}")
73
+ continue
74
+
75
+ # StreamEvent isn't exported by the SDK — duck-type on ``type``.
76
+ event_type = getattr(msg, "type", None)
77
+ if event_type == "tool_use":
78
+ tool_name = getattr(msg, "name", "unknown")
79
+ formatted_parts.append(f"[TOOL USE] {tool_name}")
80
+ continue
81
+
82
+ type_name = type(msg).__name__
83
+ # StreamEvent is a known SDK type used for token-delta capture
84
+ # elsewhere; don't surface it as an "unhandled" warning here.
85
+ if type_name == "StreamEvent":
86
+ continue
87
+ if type_name not in warned_unknown_types:
88
+ warned_unknown_types.add(type_name)
89
+ log.warning(
90
+ "Unhandled SDK message type %s in format_messages — "
91
+ + "extend the isinstance chain when the SDK adds new types.",
92
+ type_name,
93
+ )
94
+ formatted_parts.append(f"[{type_name}]")
95
+
96
+ return "\n".join(formatted_parts) if formatted_parts else "[No output]"
97
+
98
+
99
+ def format_payload(value: Any, *, max_chars: int = 800) -> str:
100
+ """Render a tool parameter or result payload as a display string.
101
+
102
+ JSON-shaped payloads (dict, list, or a string/MCP-block whose first
103
+ non-whitespace character is ``{`` or ``[``) are pretty-printed with
104
+ ``indent=2``. Everything else — plain text, strings with prefix noise —
105
+ falls through to ``str()``. Output is capped at ``max_chars`` with a
106
+ visible ``…(N more chars)`` marker.
107
+
108
+ A companion parser, ``ClaudeCodeAgent._try_parse_json_value``, handles
109
+ the telemetry path (``result_data`` capture) with stricter heuristics
110
+ for prefix noise; this helper is intentionally leaner since the fallback
111
+ to ``str()`` is harmless for display.
112
+ """
113
+ parsed = _extract_json(value)
114
+ if parsed is not None:
115
+ return _truncate(json.dumps(parsed, indent=2, default=str), max_chars)
116
+ if value is None:
117
+ return ""
118
+ return _truncate(str(value), max_chars)
119
+
120
+
121
+ def _extract_json(value: Any) -> dict[str, Any] | list[Any] | None:
122
+ """Return the non-empty JSON dict/list inside ``value``, else None."""
123
+ if isinstance(value, dict):
124
+ return value or None
125
+ if isinstance(value, list):
126
+ if value and all(
127
+ isinstance(v, dict) and v.get("type") == "text" and isinstance(v.get("text"), str) for v in value
128
+ ):
129
+ return _parse_json_string("".join(v["text"] for v in value))
130
+ return value or None
131
+ if isinstance(value, str):
132
+ return _parse_json_string(value)
133
+ return None
134
+
135
+
136
+ def _parse_json_string(text: str) -> dict[str, Any] | list[Any] | None:
137
+ """Parse ``text`` as JSON when it starts (after ``lstrip``) with ``{`` or
138
+ ``[``. Tolerates trailing garbage via ``raw_decode``. Rejects empty
139
+ containers so accidental matches don't suppress the ``str()`` fallback.
140
+ Does NOT strip leading non-whitespace prefix lines — strings like
141
+ ``"Warning: foo\\n{...}"`` fall through to ``str()``.
142
+ """
143
+ stripped = text.lstrip()
144
+ if not stripped or stripped[0] not in "{[":
145
+ return None
146
+ try:
147
+ parsed, _ = json.JSONDecoder().raw_decode(stripped)
148
+ except json.JSONDecodeError:
149
+ return None
150
+ if isinstance(parsed, (dict, list)) and parsed:
151
+ return parsed
152
+ return None
153
+
154
+
155
+ def _truncate(text: str, max_chars: int) -> str:
156
+ """Truncate ``text`` to ``max_chars``, appending a visible marker."""
157
+ if len(text) <= max_chars:
158
+ return text
159
+ dropped = len(text) - max_chars
160
+ return f"{text[:max_chars]}…({dropped} more chars)"
161
+
162
+
163
+ def format_token_usage(usage: Any) -> str:
164
+ """Format TokenUsage into a short display string like 'in=1234, out=567'.
165
+
166
+ Returns empty string if usage is None or has no token counts.
167
+ Includes cache_read_input_tokens if non-zero.
168
+
169
+ ``in=`` shows ``uncached_input_tokens`` (the fresh, billed-at-input-rate
170
+ slice), NOT the derived ``input_tokens`` total — the total already folds in
171
+ ``cache_read``, so using it here while also appending ``cached=`` would
172
+ double-count the cached prefix. Falls back to ``input_tokens`` only for
173
+ objects that predate the split / don't expose the uncached field.
174
+ """
175
+ if usage is None:
176
+ return ""
177
+ try:
178
+ input_tokens = getattr(usage, "uncached_input_tokens", None)
179
+ if input_tokens is None:
180
+ input_tokens = getattr(usage, "input_tokens", 0)
181
+ input_tokens = input_tokens or 0
182
+ output_tokens = getattr(usage, "output_tokens", 0) or 0
183
+ if not input_tokens and not output_tokens:
184
+ return ""
185
+ parts = [f"in={input_tokens}", f"out={output_tokens}"]
186
+ cached = getattr(usage, "cache_read_input_tokens", 0) or 0
187
+ if cached:
188
+ parts.append(f"cached={cached}")
189
+ return ", ".join(parts)
190
+ except (AttributeError, TypeError):
191
+ return ""
@@ -0,0 +1,15 @@
1
+ """Per-task Docker isolation.
2
+
3
+ The host dispatches each task as ``docker run --rm coder-eval-agent ...``.
4
+ Inside the container, the same Orchestrator/Sandbox/Agent code runs in-process
5
+ with ``driver: tempdir``, writes ``task.json``, and exits. The host reads
6
+ ``task.json`` from a bind-mounted output dir and feeds it to the normal
7
+ aggregation pipeline.
8
+
9
+ Aggregation (P/R/F1, suite thresholds, reports) always stays on the host.
10
+ """
11
+
12
+ from .docker_runner import DockerRunner
13
+
14
+
15
+ __all__ = ["DockerRunner"]