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,240 @@
1
+ """Error categorization logic using pattern matching.
2
+
3
+ This module provides the categorize_error function that analyzes exceptions
4
+ and maps them to appropriate ErrorCategory values for retry logic.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from .agent import AgentConfigError, AgentCrashError
11
+ from .budget import BudgetExceededError
12
+ from .categories import ErrorCategory
13
+ from .timeout import EvaluationTimeoutError
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _categorize_by_exception_type(error: Exception, component: str) -> ErrorCategory | None:
20
+ """Typed-exception checks (most reliable), in precedence order.
21
+
22
+ Returns None when no typed check matches, so the caller falls through to
23
+ string-pattern matching. ``AgentCrashError`` is deliberately NOT checked here:
24
+ it is the last resort (see ``categorize_error``), after string patterns get a
25
+ chance to recognise auth / rate-limit / billing / content-filter signatures
26
+ stamped into the message. The component-gated branches also return ``None``
27
+ when their component doesn't match, so non-agent errors fall through to the
28
+ component group (e.g. a sandbox TimeoutError reaches the sandbox categories
29
+ with their designed retry behavior instead of dead-ending at ``UNKNOWN``).
30
+ """
31
+ # Check custom timeout exceptions first (before generic TimeoutError)
32
+ if isinstance(error, EvaluationTimeoutError):
33
+ return ErrorCategory.AGENT_TIMEOUT
34
+
35
+ # RunLimits budget breach — typed, distinct from time-based exceptions.
36
+ if isinstance(error, BudgetExceededError):
37
+ return ErrorCategory.BUDGET_EXCEEDED
38
+
39
+ # AgentConfigError subclasses RuntimeError, so this typed check must precede
40
+ # any string-pattern fallback that could re-categorise a missing-prerequisite
41
+ # message as the retryable AGENT_API_ERROR.
42
+ if isinstance(error, AgentConfigError):
43
+ return ErrorCategory.AGENT_CONFIG_ERROR
44
+
45
+ if isinstance(error, TimeoutError):
46
+ if component == "agent":
47
+ return ErrorCategory.AGENT_TIMEOUT
48
+ return None # fall through: let the component group categorize it
49
+
50
+ if isinstance(error, FileNotFoundError):
51
+ if "task" in str(error).lower() or "yaml" in str(error).lower():
52
+ return ErrorCategory.TASK_NOT_FOUND
53
+ return None # fall through: let the component group categorize it
54
+
55
+ if isinstance(error, MemoryError):
56
+ return ErrorCategory.OUT_OF_MEMORY
57
+
58
+ # Check for known SDK exceptions (if available)
59
+ try:
60
+ from anthropic import AuthenticationError, RateLimitError
61
+
62
+ if isinstance(error, RateLimitError):
63
+ return ErrorCategory.AGENT_RATE_LIMIT
64
+ if isinstance(error, AuthenticationError):
65
+ return ErrorCategory.AGENT_AUTH_ERROR
66
+ except ImportError:
67
+ pass # anthropic package not installed; fall through to string matching
68
+
69
+ return None
70
+
71
+
72
+ def _categorize_by_message(error_str: str, component: str) -> ErrorCategory | None:
73
+ """String-pattern matching on the already-lowercased error message.
74
+
75
+ ``error_str`` is the caller's ``str(error).lower()``. Patterns run in precedence
76
+ order; returns None when no pattern matches (fall through to component
77
+ categorization). The component-gated branches also return ``None`` for
78
+ non-agent components, so e.g. a sandbox pip failure mentioning "connection"
79
+ reaches ``PACKAGE_INSTALL_ERROR`` (retryable) instead of ``UNKNOWN``.
80
+ """
81
+ # Authentication errors
82
+ if any(pat in error_str for pat in ["authentication", "unauthorized", "invalid api key", "401"]):
83
+ return ErrorCategory.AGENT_AUTH_ERROR
84
+
85
+ # Billing/credit errors (NOT retryable - retrying wastes time)
86
+ # NOTE: Broad patterns like "insufficient" and "credit" are intentional. We prefer
87
+ # false positives (skipping retry on a non-billing error) over false negatives
88
+ # (wasting retries on a billing error that will never succeed).
89
+ if any(
90
+ pat in error_str
91
+ for pat in ["credit", "billing", "payment", "insufficient", "402", "quota exceeded", "spending limit"]
92
+ ):
93
+ return ErrorCategory.AGENT_BILLING_ERROR
94
+
95
+ # Rate limiting
96
+ if any(pat in error_str for pat in ["rate limit", "429", "ratelimit", "too many requests"]):
97
+ return ErrorCategory.AGENT_RATE_LIMIT
98
+
99
+ # Timeouts
100
+ if "timeout" in error_str:
101
+ if component == "agent":
102
+ return ErrorCategory.AGENT_TIMEOUT
103
+ return None # fall through: let the component group categorize it
104
+
105
+ # Content filtering (Bedrock guardrails) — NOT retryable, same output will be blocked again
106
+ if any(pat in error_str for pat in ["content filtering policy", "content filter", "guardrail"]):
107
+ return ErrorCategory.AGENT_INVALID_OUTPUT
108
+
109
+ # API/Network errors
110
+ if any(pat in error_str for pat in ["api error", "connection", "network", "502", "503", "504"]):
111
+ if component == "agent":
112
+ return ErrorCategory.AGENT_API_ERROR
113
+ return None # fall through: let the component group categorize it
114
+
115
+ # Disk errors
116
+ if any(pat in error_str for pat in ["disk", "no space", "enospc"]):
117
+ return ErrorCategory.DISK_FULL
118
+
119
+ # Memory errors
120
+ if any(pat in error_str for pat in ["memory", "oom", "out of memory"]):
121
+ return ErrorCategory.OUT_OF_MEMORY
122
+
123
+ return None
124
+
125
+
126
+ def _categorize_by_component(error: Exception, error_str: str, component: str) -> ErrorCategory | None:
127
+ """Component-specific categorization for callers that supplied a component hint.
128
+
129
+ Returns None **only** when ``component`` is not one of sandbox/agent/evaluator/task,
130
+ so the main function reaches the final AgentCrashError / UNKNOWN fallback.
131
+ """
132
+ if component == "sandbox":
133
+ if "venv" in error_str or "virtualenv" in error_str:
134
+ return ErrorCategory.VENV_CREATION_ERROR
135
+ if "install" in error_str or "pip" in error_str or "package" in error_str:
136
+ return ErrorCategory.PACKAGE_INSTALL_ERROR
137
+ if "git" in error_str or "clone" in error_str:
138
+ return ErrorCategory.GIT_CLONE_ERROR
139
+ if "template" in error_str or "copy" in error_str:
140
+ return ErrorCategory.TEMPLATE_COPY_ERROR
141
+ return ErrorCategory.SANDBOX_SETUP_ERROR
142
+
143
+ if component == "agent":
144
+ # Substring heuristics for plain RuntimeErrors that mention a crash
145
+ # but aren't typed as AgentCrashError. The typed-AgentCrashError
146
+ # check below catches the agent's wrapped exceptions; this branch
147
+ # only fires for bare exceptions whose message happens to describe
148
+ # a crash.
149
+ if (
150
+ "crash" in error_str
151
+ or "killed" in error_str
152
+ or "cli process failed (exit code" in error_str
153
+ or "command failed with exit code" in error_str
154
+ ):
155
+ return ErrorCategory.AGENT_CRASH
156
+ if "invalid" in error_str or "malformed" in error_str:
157
+ return ErrorCategory.AGENT_INVALID_OUTPUT
158
+
159
+ # Typed agent-crash exception is the LAST agent-side resort: by this
160
+ # point pattern matching has had a chance to recognise auth / rate-
161
+ # limit / billing / content-filter / api-network signatures stamped
162
+ # into the message, so anything still typed as AgentCrashError here
163
+ # is a genuinely unexpected failure that the user wants retried.
164
+ if isinstance(error, AgentCrashError):
165
+ return ErrorCategory.AGENT_CRASH
166
+
167
+ # Default to API error for unknown agent failures (more likely to be retryable)
168
+ return ErrorCategory.AGENT_API_ERROR
169
+
170
+ if component == "evaluator":
171
+ return ErrorCategory.CRITERION_CHECK_ERROR
172
+
173
+ if component == "task":
174
+ if "invalid" in error_str or "malformed" in error_str or "validation" in error_str:
175
+ return ErrorCategory.TASK_INVALID
176
+ return ErrorCategory.TASK_NOT_FOUND
177
+
178
+ return None
179
+
180
+
181
+ def categorize_error(
182
+ error: Exception,
183
+ context: dict[str, Any],
184
+ hint: ErrorCategory | None = None,
185
+ ) -> ErrorCategory:
186
+ """Categorize an exception into an ErrorCategory.
187
+
188
+ Uses pattern matching on error message and exception type,
189
+ plus context hints (component name) to determine category.
190
+
191
+ Prioritizes:
192
+ 1. Explicit hint (if provided by caller who knows the context)
193
+ 2. Exception type checking (most reliable)
194
+ 3. Known SDK exceptions (if available)
195
+ 4. String pattern matching (fallback)
196
+
197
+ Args:
198
+ error: The exception to categorize
199
+ context: Additional context dict with keys:
200
+ - component: "agent", "sandbox", "evaluator", etc.
201
+ - task_id: Task identifier (for logging)
202
+ hint: Optional explicit category (when caller knows the type)
203
+
204
+ Returns:
205
+ ErrorCategory enum value
206
+
207
+ Example:
208
+ >>> categorize_error(TimeoutError(), {"component": "agent"})
209
+ <ErrorCategory.AGENT_TIMEOUT: 'agent_timeout'>
210
+ >>> categorize_error(Exception("Rate limit"), {"component": "agent"})
211
+ <ErrorCategory.AGENT_RATE_LIMIT: 'agent_rate_limit'>
212
+ """
213
+ # Use hint if provided (caller knows best)
214
+ if hint:
215
+ return hint
216
+
217
+ component = context.get("component", "")
218
+
219
+ # Precedence: typed-exception group → message-string group → component group.
220
+ # A helper returning None means "no match in my group, fall through" — including
221
+ # the component-gated timeout/network arms for non-agent components, so a
222
+ # sandbox failure reaches the sandbox categories (and their retries) below.
223
+ if (category := _categorize_by_exception_type(error, component)) is not None:
224
+ return category
225
+
226
+ error_str = str(error).lower()
227
+ if (category := _categorize_by_message(error_str, component)) is not None:
228
+ return category
229
+
230
+ if (category := _categorize_by_component(error, error_str, component)) is not None:
231
+ return category
232
+
233
+ # Final typed-AgentCrashError fallback for callers without a component hint:
234
+ # nothing more specific matched, treat as a genuinely unexpected crash.
235
+ if isinstance(error, AgentCrashError):
236
+ return ErrorCategory.AGENT_CRASH
237
+
238
+ # Default to unknown
239
+ logger.warning(f"Could not categorize error: {error} (component={component})")
240
+ return ErrorCategory.UNKNOWN
@@ -0,0 +1,124 @@
1
+ """Retry executor - main retry orchestration logic."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
7
+
8
+ from .categories import RETRY_CONFIG, RetryConfig
9
+ from .categorization import categorize_error
10
+ from .retry import get_error_tip, get_retry_delay, should_retry
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def execute_with_retry(
17
+ operation: Callable[[], Awaitable[Any]],
18
+ operation_name: str,
19
+ context: dict[str, Any],
20
+ max_attempts: int | None = None,
21
+ on_attempt_error: Callable[[Exception, int], Awaitable[None]] | None = None,
22
+ ) -> Any:
23
+ """Execute an operation with automatic retry on transient errors.
24
+
25
+ This is the core retry mechanism. It wraps any async operation and:
26
+ 1. Executes the operation
27
+ 2. Catches exceptions
28
+ 3. Categorizes the error
29
+ 4. Retries if eligible (with exponential backoff + jitter)
30
+ 5. Re-raises after exhausting retries
31
+
32
+ Args:
33
+ operation: Async callable to execute (no arguments, use closures/lambdas)
34
+ operation_name: Human-readable operation name for logging
35
+ context: Context dict with:
36
+ - task_id: Task identifier (required)
37
+ - component: Component name (optional but recommended)
38
+ - agent_name: Agent name (optional)
39
+ max_attempts: Override max attempts (defaults to 10 as safety limit)
40
+ on_attempt_error: Async ``(exception, zero_indexed_attempt) -> None`` callback
41
+ invoked after every failed attempt (including the final non-retryable one),
42
+ for draining ``agent.pending_turn`` and calling ``agent.discard_pending_turn()``.
43
+ Callback exceptions are logged and swallowed so they cannot mask the original.
44
+
45
+ Returns:
46
+ Result from operation
47
+
48
+ Raises:
49
+ Last exception if all retries exhausted
50
+
51
+ Example:
52
+ >>> async def flaky_api_call():
53
+ ... return await agent.communicate(prompt)
54
+ >>>
55
+ >>> result = await execute_with_retry(
56
+ ... operation=flaky_api_call,
57
+ ... operation_name="Agent communication",
58
+ ... context={"task_id": "task-001", "component": "agent"},
59
+ ... )
60
+ """
61
+ task_id = context.get("task_id", "unknown")
62
+ last_error = None
63
+
64
+ # Determine max attempts (safety limit)
65
+ if max_attempts is None:
66
+ max_attempts = 10
67
+
68
+ for attempt in range(max_attempts):
69
+ try:
70
+ # Execute operation
71
+ return await operation()
72
+
73
+ except asyncio.CancelledError:
74
+ # Re-raise cancellation immediately - not an error to retry
75
+ raise
76
+
77
+ except Exception as e:
78
+ last_error = e
79
+
80
+ # Fire callback before the retry decision so partial telemetry
81
+ # is captured even on the final non-retryable attempt.
82
+ if on_attempt_error is not None:
83
+ try:
84
+ await on_attempt_error(e, attempt)
85
+ except Exception:
86
+ logger.exception(
87
+ "[%s] on_attempt_error callback raised for %s (attempt %d); ignoring",
88
+ task_id,
89
+ operation_name,
90
+ attempt + 1,
91
+ )
92
+
93
+ # Categorize error
94
+ category = categorize_error(e, context)
95
+ config = RETRY_CONFIG.get(category, RetryConfig())
96
+
97
+ # Check if we should retry (handles both non-retryable categories and exhausted attempts)
98
+ if not should_retry(category, attempt):
99
+ if config.max_retries == 0:
100
+ logger.error(f"[{task_id}] {operation_name} failed (non-retryable): {category.value} - {e}")
101
+ else:
102
+ logger.error(
103
+ f"[{task_id}] {operation_name} failed after {attempt + 1} attempts: {category.value} - {e}"
104
+ )
105
+ raise
106
+
107
+ # Calculate backoff with jitter
108
+ delay = get_retry_delay(category, attempt)
109
+
110
+ # Enhanced warning with actionable tip
111
+ tip = get_error_tip(category)
112
+ message = (
113
+ f"[{task_id}] {operation_name} failed (attempt {attempt + 1}/{config.max_retries + 1}): "
114
+ f"{category.value} - {e}. Retrying in {delay:.1f}s...\n 💡 Tip: {tip}"
115
+ )
116
+ logger.warning(message)
117
+
118
+ # Async sleep (non-blocking)
119
+ await asyncio.sleep(delay)
120
+
121
+ # Should never reach here, but just in case
122
+ if last_error:
123
+ raise last_error
124
+ raise RuntimeError(f"Unexpected retry loop exit for {operation_name}")
@@ -0,0 +1,12 @@
1
+ """Typed error for judge-side infrastructure failures."""
2
+
3
+
4
+ class JudgeInfrastructureError(RuntimeError):
5
+ """Judge-side infrastructure failure (transport error, throttle, 5xx, auth)
6
+ after bounded retries.
7
+
8
+ Deliberately NOT converted to a scored-0.0 ``CriterionResult``:
9
+ ``handle_criterion_errors`` and ``SuccessChecker._check_single`` re-raise it
10
+ so the orchestrator lands the row as ``FinalStatus.ERROR`` — a judge outage
11
+ must be distinguishable from genuine agent failure in pass-rate metrics.
12
+ """
@@ -0,0 +1,211 @@
1
+ """Retry logic with exponential backoff and error context management.
2
+
3
+ This module provides retry functionality, error context creation, and helper
4
+ utilities for production-grade error handling.
5
+ """
6
+
7
+ import logging
8
+ import random
9
+ import traceback
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from .categories import ERROR_TIPS, RETRY_CONFIG, ErrorCategory, ErrorContext, RetryConfig
14
+ from .categorization import categorize_error
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def should_retry(category: ErrorCategory, attempt: int) -> bool:
21
+ """Determine if an error should be retried.
22
+
23
+ Args:
24
+ category: Error category
25
+ attempt: Current attempt number (0-indexed)
26
+
27
+ Returns:
28
+ True if retry should be attempted
29
+ """
30
+ config = RETRY_CONFIG.get(category, RetryConfig())
31
+ return attempt < config.max_retries
32
+
33
+
34
+ def compute_backoff(config: RetryConfig, attempt: int) -> float:
35
+ """Calculate delay with exponential backoff and jitter.
36
+
37
+ Formula: (initial_delay * backoff_multiplier^attempt) + jitter
38
+ Jitter: Random value between 0 and 25% of base delay
39
+
40
+ Args:
41
+ config: Retry configuration (initial delay, multiplier)
42
+ attempt: Current attempt number (0-indexed)
43
+
44
+ Returns:
45
+ Delay in seconds
46
+ """
47
+ base_delay = config.initial_delay * (config.backoff_multiplier**attempt)
48
+
49
+ # Add jitter: up to 25% of base delay to prevent thundering herd
50
+ jitter = random.uniform(0, base_delay * 0.25)
51
+
52
+ return base_delay + jitter
53
+
54
+
55
+ def get_retry_delay(category: ErrorCategory, attempt: int) -> float:
56
+ """Calculate delay before retry with exponential backoff and jitter.
57
+
58
+ Convenience wrapper around ``compute_backoff`` that looks up the
59
+ ``RetryConfig`` for the given error category.
60
+
61
+ Args:
62
+ category: Error category
63
+ attempt: Current attempt number (0-indexed)
64
+
65
+ Returns:
66
+ Delay in seconds
67
+
68
+ Example:
69
+ >>> get_retry_delay(ErrorCategory.AGENT_API_ERROR, 0) # doctest: +SKIP
70
+ 5.2 # 5.0 base + 0.2 jitter
71
+ >>> get_retry_delay(ErrorCategory.AGENT_API_ERROR, 1) # doctest: +SKIP
72
+ 10.8 # 10.0 base + 0.8 jitter
73
+ """
74
+ config = RETRY_CONFIG.get(category, RetryConfig())
75
+ return compute_backoff(config, attempt)
76
+
77
+
78
+ def get_error_tip(category: ErrorCategory) -> str:
79
+ """Get actionable tip for error category.
80
+
81
+ Args:
82
+ category: Error category
83
+
84
+ Returns:
85
+ Actionable tip string
86
+ """
87
+ return ERROR_TIPS.get(category, "Check logs for details or run with --verbose for more information.")
88
+
89
+
90
+ def truncate_log(log: str, max_chars: int = 1000) -> str:
91
+ """Truncate log preserving both start and end.
92
+
93
+ Keeps first 40% and last 60% of the log instead of just the end.
94
+ This preserves important startup information while still showing
95
+ the final error state.
96
+
97
+ Args:
98
+ log: Log string to truncate
99
+ max_chars: Maximum characters to keep (default: 1000)
100
+
101
+ Returns:
102
+ Truncated log string with separator if truncated
103
+
104
+ Example:
105
+ >>> truncate_log("x" * 100, max_chars=100)
106
+ 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
107
+ >>> len(truncate_log("x" * 2000, max_chars=1000))
108
+ 1000
109
+ """
110
+ if len(log) <= max_chars:
111
+ return log
112
+
113
+ # Keep first 40% and last 60% with separator
114
+ separator = "\n\n... [truncated] ...\n\n"
115
+ head_size = int(max_chars * 0.4)
116
+ tail_size = max(0, max_chars - head_size - len(separator))
117
+
118
+ return log[:head_size] + separator + log[-tail_size:]
119
+
120
+
121
+ def error_context_to_dict(ctx: ErrorContext) -> dict[str, Any]:
122
+ """Convert ErrorContext to dict for EvaluationResult.error_details.
123
+
124
+ Args:
125
+ ctx: ErrorContext instance
126
+
127
+ Returns:
128
+ Dict representation of error context with error_tip added
129
+ """
130
+ result = ctx.model_dump()
131
+ # Add error tip for this error category
132
+ category = ErrorCategory(ctx.error_category)
133
+ result["error_tip"] = get_error_tip(category)
134
+ return result
135
+
136
+
137
+ def create_error_context(
138
+ error: Exception,
139
+ task_id: str,
140
+ attempt: int,
141
+ component: str | None = None,
142
+ agent_name: str | None = None,
143
+ **kwargs: Any,
144
+ ) -> dict[str, Any]:
145
+ """Create comprehensive error context for reporting.
146
+
147
+ Internally creates a type-safe ErrorContext model, then converts to dict
148
+ for backward compatibility with EvaluationResult.error_details.
149
+
150
+ Args:
151
+ error: The exception that occurred
152
+ task_id: Task identifier
153
+ attempt: Attempt number (1-indexed)
154
+ component: Component that failed (agent/sandbox/evaluator)
155
+ agent_name: Agent name (optional)
156
+ **kwargs: Additional context fields:
157
+ - disk_usage_gb: Disk usage in GB
158
+ - memory_usage_gb: Memory usage in GB
159
+ - agent_stdout: Agent stdout (will be truncated)
160
+ - agent_stderr: Agent stderr (will be truncated)
161
+
162
+ Returns:
163
+ Dict with error context fields (from ErrorContext.model_dump())
164
+
165
+ Example:
166
+ >>> ctx = create_error_context(
167
+ ... error=ValueError("test"),
168
+ ... task_id="task-001",
169
+ ... attempt=1,
170
+ ... component="agent"
171
+ ... )
172
+ >>> ctx["task_id"]
173
+ 'task-001'
174
+ >>> ctx["attempt_number"]
175
+ 1
176
+ """
177
+ # Categorize error
178
+ category = categorize_error(error, {"component": component, "task_id": task_id})
179
+
180
+ # Truncate logs (improved: keeps start + end)
181
+ agent_stdout = kwargs.get("agent_stdout")
182
+ if agent_stdout:
183
+ agent_stdout = truncate_log(agent_stdout)
184
+
185
+ agent_stderr = kwargs.get("agent_stderr")
186
+ if agent_stderr:
187
+ agent_stderr = truncate_log(agent_stderr)
188
+
189
+ # Convert to 0-indexed attempt (defensive: prevent negative indexing)
190
+ attempt_index = max(attempt - 1, 0)
191
+
192
+ # Create type-safe ErrorContext model
193
+ ctx = ErrorContext(
194
+ error_category=category.value,
195
+ error_message=str(error),
196
+ stack_trace=traceback.format_exc(),
197
+ task_id=task_id,
198
+ component=component,
199
+ agent_name=agent_name,
200
+ attempt_number=attempt,
201
+ is_retryable=should_retry(category, attempt_index),
202
+ retry_delay_seconds=get_retry_delay(category, attempt_index) if should_retry(category, attempt_index) else 0.0,
203
+ disk_usage_gb=kwargs.get("disk_usage_gb"),
204
+ memory_usage_gb=kwargs.get("memory_usage_gb"),
205
+ agent_stdout=agent_stdout,
206
+ agent_stderr=agent_stderr,
207
+ timestamp=datetime.now().isoformat(),
208
+ )
209
+
210
+ # Return dict for backward compatibility
211
+ return error_context_to_dict(ctx)
@@ -0,0 +1,63 @@
1
+ """Timeout exceptions for evaluation lifecycle."""
2
+
3
+ from .agent import format_timeout_reason
4
+
5
+
6
+ class EvaluationTimeoutError(Exception):
7
+ """Base timeout error for evaluation lifecycle.
8
+
9
+ Wraps asyncio.TimeoutError with structured context about
10
+ what timed out and where.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ timeout_seconds: float,
18
+ layer: str, # "turn" | "task"
19
+ task_id: str | None = None,
20
+ iteration: int | None = None,
21
+ elapsed_seconds: float | None = None,
22
+ ):
23
+ super().__init__(message)
24
+ self.timeout_seconds = timeout_seconds
25
+ self.layer = layer
26
+ self.task_id = task_id
27
+ self.iteration = iteration
28
+ self.elapsed_seconds = elapsed_seconds
29
+
30
+
31
+ class TurnTimeoutError(EvaluationTimeoutError):
32
+ """Agent turn (communicate) exceeded its time limit."""
33
+
34
+ def __init__(
35
+ self,
36
+ timeout_seconds: float,
37
+ *,
38
+ task_id: str | None = None,
39
+ iteration: int | None = None,
40
+ ):
41
+ message = format_timeout_reason(timeout_seconds)
42
+ if iteration is not None:
43
+ message = f"{message} (iteration {iteration})"
44
+ super().__init__(
45
+ message,
46
+ timeout_seconds=timeout_seconds,
47
+ layer="turn",
48
+ task_id=task_id,
49
+ iteration=iteration,
50
+ )
51
+
52
+
53
+ class TaskTimeoutError(EvaluationTimeoutError):
54
+ """Overall task evaluation loop exceeded its time limit."""
55
+
56
+ def __init__(self, timeout_seconds: float, *, task_id: str | None = None, elapsed_seconds: float | None = None):
57
+ super().__init__(
58
+ f"Task timed out after {timeout_seconds}s",
59
+ timeout_seconds=timeout_seconds,
60
+ layer="task",
61
+ task_id=task_id,
62
+ elapsed_seconds=elapsed_seconds,
63
+ )
@@ -0,0 +1,10 @@
1
+ """Evaluation components for checking success criteria and providing LLM feedback.
2
+
3
+ This package contains:
4
+ - checker: Success criterion validation with continuous scoring (0.0-1.0)
5
+ - summaries: summarize_commands helper shared by orchestrator + llm_judge
6
+
7
+ NO re-exports - use explicit imports:
8
+ from coder_eval.evaluation.checker import SuccessChecker
9
+ from coder_eval.evaluation.summaries import summarize_commands
10
+ """