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,260 @@
1
+ """Success criterion checking with pluggable criterion types.
2
+
3
+ This module provides the SuccessChecker class which orchestrates
4
+ criterion checking using the registered criterion checkers from
5
+ the criteria registry.
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from ..criteria import BaseCriterion, CriterionRegistry, init_criteria
13
+ from ..criteria.base import CheckContext
14
+ from ..errors import JudgeInfrastructureError
15
+ from ..models import CriteriaResults, CriterionResult, SuccessCriteria, SuccessCriterion, TurnRecords
16
+ from ..sandbox import Sandbox
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from ..models.routing import ApiRoute
21
+
22
+
23
+ # Get module logger
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _short_failure_reason(result: CriterionResult, max_len: int = 200) -> str:
28
+ """Return a one-line, length-capped failure reason for a criterion result.
29
+
30
+ Prefers ``result.error``; then falls back to the first ``stderr:`` line in
31
+ ``result.details``; then the first non-empty details line. Multi-line text
32
+ is collapsed with ``" | "`` separators before truncation. Returns
33
+ ``"no details"`` when nothing usable is available.
34
+
35
+ Shared by ``SuccessChecker`` (console fail log) and
36
+ ``orchestrator._emit_criteria_event`` (streamed ``failure_reason`` field)
37
+ so both surfaces show identical text.
38
+ """
39
+
40
+ def _collapse(text: str) -> str:
41
+ lines = [line.strip() for line in text.splitlines()]
42
+ collapsed = " | ".join(line for line in lines if line)
43
+ if len(collapsed) > max_len:
44
+ return collapsed[: max_len - 1] + "…"
45
+ return collapsed
46
+
47
+ if result.error:
48
+ return _collapse(result.error) or "no details"
49
+ if result.details:
50
+ for line in result.details.splitlines():
51
+ stripped = line.strip()
52
+ if stripped.lower().startswith("stderr:"):
53
+ reason = stripped[len("stderr:") :].strip()
54
+ if reason:
55
+ return _collapse(reason)
56
+ for line in result.details.splitlines():
57
+ stripped = line.strip()
58
+ if stripped:
59
+ return _collapse(stripped)
60
+ return "no details"
61
+
62
+
63
+ class SuccessChecker:
64
+ """Orchestrates criterion checking using registered checkers."""
65
+
66
+ def __init__(
67
+ self,
68
+ sandbox: Sandbox,
69
+ init_registry: bool = True,
70
+ validate_registry: bool = True,
71
+ route: "ApiRoute | None" = None,
72
+ ):
73
+ """Initialize the success checker.
74
+
75
+ Args:
76
+ sandbox: Sandbox instance for running checks
77
+ init_registry: Whether to initialize the criteria registry
78
+ validate_registry: Whether to validate all expected types are registered
79
+ route: Resolved ``ApiRoute`` from the orchestrator. Forwarded to every
80
+ checker's ``check()`` so criteria that spawn sub-agents (e.g.
81
+ ``agent_judge``) can route through the same backend (Direct /
82
+ Bedrock) as the main coding agent. ``None`` is acceptable
83
+ for non-sub-agent criteria; ``agent_judge`` requires a route.
84
+ """
85
+ self.sandbox = sandbox
86
+ self._checker_instances: dict[str, BaseCriterion[Any]] = {}
87
+ # Cached reference code - automatically set by check()/check_all() when provided
88
+ # Used by subsequent check() calls that don't explicitly pass reference_code
89
+ self._reference_code: str | None = None
90
+ # Cached reference directory path (resolved). Set by check()/check_all() when provided.
91
+ # Mutually exclusive with self._reference_code at the task level — task.reference
92
+ # is exactly one of code/file/directory.
93
+ self._reference_dir: Path | None = None
94
+ # Cached turn records - set by check()/check_all() when provided
95
+ self._turn_records: TurnRecords | None = None
96
+ self.route = route
97
+
98
+ # V3: Lazy initialization - registry loaded here, not at import
99
+ if init_registry:
100
+ init_criteria(validate=validate_registry)
101
+
102
+ def check(
103
+ self,
104
+ criterion: SuccessCriterion,
105
+ reference_code: str | None = None,
106
+ turn_records: TurnRecords | None = None,
107
+ reference_dir: Path | None = None,
108
+ ) -> CriterionResult:
109
+ """Check a single criterion (backward compatibility wrapper).
110
+
111
+ Args:
112
+ criterion: Criterion definition
113
+ reference_code: Optional reference code (string form: from
114
+ ``task.reference.code`` or ``task.reference.file``).
115
+ turn_records: Optional turn records for command inspection
116
+ reference_dir: Optional resolved path to a reference directory
117
+ (from ``task.reference.directory``). Only consumed by
118
+ ``agent_judge``; non-judge criteria ignore it.
119
+
120
+ Returns:
121
+ CriterionResult with score
122
+ """
123
+ # Persist reference_code / reference_dir for subsequent calls (backward compat)
124
+ if reference_code is not None:
125
+ self._reference_code = reference_code
126
+ if reference_dir is not None:
127
+ self._reference_dir = reference_dir
128
+ if turn_records is not None:
129
+ self._turn_records = turn_records
130
+ ref_code = reference_code if reference_code is not None else self._reference_code
131
+ ref_dir = reference_dir if reference_dir is not None else self._reference_dir
132
+ records = turn_records if turn_records is not None else self._turn_records
133
+ return self._check_single(criterion, ref_code, records, ref_dir)
134
+
135
+ def check_all(
136
+ self,
137
+ criteria: SuccessCriteria,
138
+ reference_code: str | None = None,
139
+ turn_records: TurnRecords | None = None,
140
+ reference_dir: Path | None = None,
141
+ ) -> CriteriaResults:
142
+ """Check all success criteria.
143
+
144
+ Args:
145
+ criteria: List of criterion definitions
146
+ reference_code: Optional reference code (string form).
147
+ turn_records: Optional turn records for command inspection
148
+ reference_dir: Optional resolved path to a reference directory.
149
+ Only consumed by ``agent_judge``; non-judge criteria ignore it.
150
+
151
+ Returns:
152
+ List of criterion results with scores
153
+ """
154
+ # Persist for subsequent check() calls
155
+ if reference_code is not None:
156
+ self._reference_code = reference_code
157
+ if reference_dir is not None:
158
+ self._reference_dir = reference_dir
159
+ if turn_records is not None:
160
+ self._turn_records = turn_records
161
+ ref_code = reference_code if reference_code is not None else self._reference_code
162
+ ref_dir = reference_dir if reference_dir is not None else self._reference_dir
163
+ records = turn_records if turn_records is not None else self._turn_records
164
+ results = []
165
+ for criterion in criteria:
166
+ result = self._check_single(criterion, ref_code, records, ref_dir)
167
+ results.append(result)
168
+ return results
169
+
170
+ def _get_checker_instance(self, criterion_type: str) -> BaseCriterion[Any]:
171
+ """Get or create a checker instance (V3: cached).
172
+
173
+ Args:
174
+ criterion_type: The criterion type
175
+
176
+ Returns:
177
+ Checker instance (reused within this evaluation run)
178
+ """
179
+ if criterion_type not in self._checker_instances:
180
+ checker_class = CriterionRegistry.get_checker(criterion_type)
181
+ self._checker_instances[criterion_type] = checker_class()
182
+ return self._checker_instances[criterion_type]
183
+
184
+ def _check_single(
185
+ self,
186
+ criterion: SuccessCriterion,
187
+ reference_code: str | None,
188
+ turn_records: TurnRecords | None = None,
189
+ reference_dir: Path | None = None,
190
+ ) -> CriterionResult:
191
+ """Check a single criterion using registered checker.
192
+
193
+ Args:
194
+ criterion: Criterion definition (discriminated union)
195
+ reference_code: Optional reference code (string form)
196
+ turn_records: Optional turn records for command inspection
197
+ reference_dir: Optional resolved path to a reference directory.
198
+
199
+ Returns:
200
+ CriterionResult with score
201
+ """
202
+ criterion_type = criterion.type
203
+
204
+ # V3: Broader exception handling - catches checker constructor failures too
205
+ try:
206
+ # Get cached instance
207
+ checker = self._get_checker_instance(criterion_type)
208
+ context = CheckContext(route=self.route, reference_dir=reference_dir)
209
+ result = checker.check(
210
+ criterion,
211
+ self.sandbox,
212
+ reference_code,
213
+ turn_records=turn_records,
214
+ context=context,
215
+ )
216
+ result.pass_threshold = criterion.pass_threshold
217
+
218
+ logger.debug(f"Criterion '{criterion_type}' score: {result.score:.2f}")
219
+ if result.score < criterion.pass_threshold:
220
+ logger.info(
221
+ "Criterion '%s' FAILED (score=%.2f, threshold=%.2f): %s",
222
+ criterion_type,
223
+ result.score,
224
+ criterion.pass_threshold,
225
+ _short_failure_reason(result),
226
+ )
227
+ return result
228
+
229
+ except KeyError:
230
+ # No checker registered for this type - return failed result for consistency.
231
+ # ERROR record carries enough context — skip the companion FAILED line.
232
+ logger.error(f"No checker found for criterion type '{criterion_type}'")
233
+ return CriterionResult(
234
+ criterion_type=criterion_type,
235
+ description=criterion.description,
236
+ score=0.0,
237
+ details=f"No checker registered for criterion type '{criterion_type}'",
238
+ error=f"Unsupported criterion type: '{criterion_type}'",
239
+ pass_threshold=criterion.pass_threshold,
240
+ )
241
+ except JudgeInfrastructureError:
242
+ raise # judge infra failure escalates to FinalStatus.ERROR; do not score it
243
+ except Exception as e:
244
+ # V3: Catch ALL exceptions, including checker __init__ failures
245
+ logger.exception(f"Checker failure for criterion '{criterion_type}': {e}")
246
+ failed = CriterionResult(
247
+ criterion_type=criterion_type,
248
+ description=criterion.description,
249
+ score=0.0,
250
+ details="Error running checker",
251
+ error=str(e),
252
+ pass_threshold=criterion.pass_threshold,
253
+ )
254
+ logger.info(
255
+ "Criterion '%s' FAILED (score=0.00, threshold=%.2f): %s",
256
+ criterion_type,
257
+ criterion.pass_threshold,
258
+ _short_failure_reason(failed),
259
+ )
260
+ return failed
@@ -0,0 +1,55 @@
1
+ """Single-completion Anthropic-SDK invoker for the Direct backend.
2
+
3
+ Hits api.anthropic.com using ANTHROPIC_API_KEY from env.
4
+
5
+ The judge issues a forced ``submit_verdict`` tool call; the SDK response is
6
+ converted to a dict via ``model_dump`` so the caller can reuse
7
+ ``extract_verdict_from_anthropic_response`` — Anthropic SDK and Bedrock share
8
+ Anthropic's native message shape (content blocks with ``type: tool_use``).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from anthropic import Anthropic, APIError
16
+
17
+ from coder_eval.errors import JudgeInfrastructureError
18
+ from coder_eval.evaluation.judge_models import to_anthropic_alias
19
+
20
+
21
+ def invoke_anthropic_judge(
22
+ *,
23
+ model: str,
24
+ system: str,
25
+ user: str,
26
+ temperature: float,
27
+ max_tokens: int,
28
+ tool_spec: dict[str, Any],
29
+ timeout_seconds: float = 120.0,
30
+ ) -> dict[str, Any]:
31
+ """One Messages call with ``tools`` + forced ``tool_choice``.
32
+
33
+ Returns the SDK response converted to a dict via ``model_dump`` so the
34
+ caller can reuse ``extract_verdict_from_anthropic_response`` — Anthropic's
35
+ native shape (content blocks with ``type: tool_use``) is identical
36
+ between this SDK call and the Bedrock httpx-direct call.
37
+ """
38
+ alias = to_anthropic_alias(model)
39
+ client = Anthropic(timeout=timeout_seconds)
40
+ with client:
41
+ try:
42
+ response = client.messages.create(
43
+ model=alias,
44
+ system=system,
45
+ messages=[{"role": "user", "content": user}],
46
+ max_tokens=max_tokens,
47
+ temperature=temperature,
48
+ tools=[tool_spec], # type: ignore[arg-type]
49
+ tool_choice={"type": "tool", "name": tool_spec["name"]},
50
+ )
51
+ except APIError as e:
52
+ # The SDK already retries transient failures internally (2 attempts
53
+ # by default) — do not add another retry loop here.
54
+ raise JudgeInfrastructureError(f"Anthropic judge API error: {e}") from e
55
+ return response.model_dump()
@@ -0,0 +1,114 @@
1
+ """Single-completion Bedrock invoker for judge-style criteria.
2
+
3
+ Narrow on purpose: one POST to bedrock-runtime, bearer-token auth, no streaming.
4
+ The judge issues a forced ``submit_verdict`` tool call; we return the parsed
5
+ response dict so the caller can walk ``content`` for ``tool_use`` blocks via
6
+ ``extract_verdict_from_anthropic_response``.
7
+
8
+ Transient failures (transport errors, 429 throttles, 5xx) are retried with
9
+ jittered exponential backoff; exhaustion and non-retryable failures raise
10
+ ``JudgeInfrastructureError`` so the row escalates to ``FinalStatus.ERROR``
11
+ instead of being scored 0.0.
12
+
13
+ agent_judge uses the Claude Code SDK subprocess instead — the two paths
14
+ intentionally do not share an HTTP client.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import time
21
+ from typing import Any
22
+
23
+ import httpx
24
+
25
+ from coder_eval.errors import JudgeInfrastructureError
26
+ from coder_eval.errors.categories import RetryConfig
27
+ from coder_eval.errors.retry import compute_backoff
28
+ from coder_eval.evaluation.judge_models import to_bedrock_model
29
+ from coder_eval.models import BedrockRoute
30
+
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ _JUDGE_RETRY = RetryConfig(max_retries=3, initial_delay=2.0, backoff_multiplier=2.0)
35
+
36
+
37
+ def _is_retryable_status(status_code: int) -> bool:
38
+ """Throttles and server-side failures are worth retrying; 4xx (auth, bad request) are not."""
39
+ return status_code == 429 or status_code >= 500
40
+
41
+
42
+ def invoke_bedrock_judge(
43
+ *,
44
+ route: BedrockRoute,
45
+ model: str,
46
+ system: str,
47
+ user: str,
48
+ temperature: float,
49
+ max_tokens: int,
50
+ tool_spec: dict[str, Any],
51
+ timeout_seconds: float = 120.0,
52
+ ) -> dict[str, Any]:
53
+ """POST a Bedrock Messages request with ``tools`` + forced ``tool_choice``.
54
+
55
+ Returns the parsed response dict so the caller can walk ``content`` for
56
+ ``tool_use`` blocks via ``extract_verdict_from_anthropic_response``.
57
+
58
+ Transport errors, 429, and 5xx responses are retried up to
59
+ ``_JUDGE_RETRY.max_retries`` times with jittered exponential backoff.
60
+
61
+ Raises:
62
+ ValueError: ``model`` empty.
63
+ JudgeInfrastructureError: retries exhausted, non-retryable HTTP failure
64
+ (e.g. 400/401/403), or a non-dict JSON body.
65
+ """
66
+ qualified = to_bedrock_model(model, route.region)
67
+ url = f"https://bedrock-runtime.{route.region}.amazonaws.com/model/{qualified}/invoke"
68
+ body = {
69
+ "anthropic_version": "bedrock-2023-05-31",
70
+ "max_tokens": max_tokens,
71
+ "temperature": temperature,
72
+ "system": system,
73
+ "messages": [{"role": "user", "content": user}],
74
+ "tools": [tool_spec],
75
+ "tool_choice": {"type": "tool", "name": tool_spec["name"]},
76
+ }
77
+ headers = {
78
+ "Authorization": f"Bearer {route.bearer_token}",
79
+ "Content-Type": "application/json",
80
+ "Accept": "application/json",
81
+ }
82
+
83
+ # invoke_bedrock_judge is sync (called via asyncio.to_thread), so a plain
84
+ # time.sleep between attempts is correct here.
85
+ attempts = _JUDGE_RETRY.max_retries + 1
86
+ last_failure = ""
87
+ last_exc: Exception | None = None
88
+ for attempt in range(attempts):
89
+ if attempt:
90
+ time.sleep(compute_backoff(_JUDGE_RETRY, attempt - 1))
91
+ try:
92
+ response = httpx.post(url, headers=headers, json=body, timeout=timeout_seconds)
93
+ except httpx.HTTPError as e:
94
+ last_failure = f"Bedrock invoke transport error: {e}"
95
+ last_exc = e
96
+ logger.warning("Bedrock judge attempt %d/%d failed: %s", attempt + 1, attempts, last_failure)
97
+ continue
98
+ if _is_retryable_status(response.status_code):
99
+ last_failure = f"Bedrock invoke failed: {response.status_code} {response.text[:500]}"
100
+ last_exc = None
101
+ logger.warning("Bedrock judge attempt %d/%d failed: %s", attempt + 1, attempts, last_failure)
102
+ continue
103
+ if response.status_code >= 300:
104
+ raise JudgeInfrastructureError(f"Bedrock invoke failed: {response.status_code} {response.text[:500]}")
105
+ try:
106
+ data = response.json()
107
+ except ValueError as e:
108
+ # A malformed/truncated 2xx body (flaky proxy/gateway) is infra, not
109
+ # agent quality — escalate like the non-dict arm below.
110
+ raise JudgeInfrastructureError(f"Bedrock response is not valid JSON: {e}") from e
111
+ if not isinstance(data, dict):
112
+ raise JudgeInfrastructureError(f"Bedrock response is not a JSON object: {str(data)[:500]}")
113
+ return data
114
+ raise JudgeInfrastructureError(f"{last_failure} (after {attempts} attempts)") from last_exc