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,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)
|
coder_eval/formatting.py
ADDED
|
@@ -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"]
|