codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Fix attempt tracking for self-correction loop prevention.
|
|
2
|
+
|
|
3
|
+
Tracks which fixes have been attempted for which errors to:
|
|
4
|
+
1. Prevent repeating the same failed fix
|
|
5
|
+
2. Detect patterns indicating we should escalate to a blocker
|
|
6
|
+
3. Provide context for escalation decisions
|
|
7
|
+
|
|
8
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FixOutcome(str, Enum):
|
|
20
|
+
"""Outcome of a fix attempt."""
|
|
21
|
+
|
|
22
|
+
SUCCESS = "success"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
PENDING = "pending"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FixAttempt:
|
|
29
|
+
"""Record of a single fix attempt.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
error_signature: Hash of the normalized error
|
|
33
|
+
fix_description: What fix was attempted
|
|
34
|
+
outcome: Result of the attempt
|
|
35
|
+
timestamp: When the attempt was made
|
|
36
|
+
file_path: File being fixed (if applicable)
|
|
37
|
+
error_type: Categorized error type (e.g., "ModuleNotFoundError")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
error_signature: str
|
|
41
|
+
fix_description: str
|
|
42
|
+
outcome: FixOutcome = FixOutcome.PENDING
|
|
43
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
44
|
+
file_path: Optional[str] = None
|
|
45
|
+
error_type: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class EscalationDecision:
|
|
50
|
+
"""Decision about whether to escalate to a blocker.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
should_escalate: Whether to create a blocker
|
|
54
|
+
reason: Why escalation is recommended
|
|
55
|
+
attempted_fixes: List of fixes that were tried
|
|
56
|
+
error_summary: Summary of the error pattern
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
should_escalate: bool
|
|
60
|
+
reason: str
|
|
61
|
+
attempted_fixes: list[str] = field(default_factory=list)
|
|
62
|
+
error_summary: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Thresholds for escalation
|
|
66
|
+
MAX_SAME_ERROR_ATTEMPTS = 3
|
|
67
|
+
MAX_SAME_FILE_ATTEMPTS = 3
|
|
68
|
+
MAX_TOTAL_FAILURES = 5
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FixAttemptTracker:
|
|
72
|
+
"""Tracks fix attempts to prevent loops and detect escalation patterns.
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
tracker = FixAttemptTracker()
|
|
76
|
+
|
|
77
|
+
# Before applying a fix, check if it's been tried
|
|
78
|
+
if tracker.was_attempted(error_msg, fix_description):
|
|
79
|
+
# Skip this fix, try something else
|
|
80
|
+
pass
|
|
81
|
+
else:
|
|
82
|
+
# Record the attempt
|
|
83
|
+
tracker.record_attempt(error_msg, fix_description, file_path="main.py")
|
|
84
|
+
# Apply the fix...
|
|
85
|
+
# Record the outcome
|
|
86
|
+
tracker.record_outcome(error_msg, fix_description, FixOutcome.FAILED)
|
|
87
|
+
|
|
88
|
+
# Check if we should escalate
|
|
89
|
+
decision = tracker.should_escalate(error_msg)
|
|
90
|
+
if decision.should_escalate:
|
|
91
|
+
# Create blocker with decision.reason and decision.attempted_fixes
|
|
92
|
+
pass
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
"""Initialize the tracker."""
|
|
97
|
+
self._attempts: list[FixAttempt] = []
|
|
98
|
+
self._error_counts: dict[str, int] = {} # error_sig -> count
|
|
99
|
+
self._file_counts: dict[str, int] = {} # file_path -> failure count
|
|
100
|
+
|
|
101
|
+
def normalize_error(self, error: str) -> str:
|
|
102
|
+
"""Normalize an error message for comparison.
|
|
103
|
+
|
|
104
|
+
Removes variable parts like:
|
|
105
|
+
- Line numbers
|
|
106
|
+
- File paths (keeps basename)
|
|
107
|
+
- Memory addresses
|
|
108
|
+
- Timestamps
|
|
109
|
+
- Specific values in quotes
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
error: Raw error message
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Normalized error string
|
|
116
|
+
"""
|
|
117
|
+
if not error:
|
|
118
|
+
return ""
|
|
119
|
+
|
|
120
|
+
normalized = error.lower()
|
|
121
|
+
|
|
122
|
+
# Remove line numbers (e.g., "line 42", "at line 123")
|
|
123
|
+
normalized = re.sub(r'\bline\s+\d+\b', 'line N', normalized)
|
|
124
|
+
normalized = re.sub(r':\d+:', ':N:', normalized)
|
|
125
|
+
|
|
126
|
+
# Remove file paths but keep the filename
|
|
127
|
+
normalized = re.sub(r'["\']?(/[^"\':\s]+/)?([^"\':\s/]+\.(py|js|ts|go|rs))["\']?',
|
|
128
|
+
r'\2', normalized)
|
|
129
|
+
|
|
130
|
+
# Remove memory addresses
|
|
131
|
+
normalized = re.sub(r'0x[0-9a-f]+', '0xADDR', normalized)
|
|
132
|
+
|
|
133
|
+
# Remove timestamps
|
|
134
|
+
normalized = re.sub(r'\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}', 'TIMESTAMP', normalized)
|
|
135
|
+
|
|
136
|
+
# Normalize quoted strings (keep the quotes but replace content)
|
|
137
|
+
normalized = re.sub(r'"[^"]{20,}"', '"..."', normalized)
|
|
138
|
+
normalized = re.sub(r"'[^']{20,}'", "'...'", normalized)
|
|
139
|
+
|
|
140
|
+
# Remove extra whitespace
|
|
141
|
+
normalized = ' '.join(normalized.split())
|
|
142
|
+
|
|
143
|
+
return normalized
|
|
144
|
+
|
|
145
|
+
def hash_error(self, error: str) -> str:
|
|
146
|
+
"""Create a hash signature for an error message.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
error: Error message (will be normalized)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Short hash string
|
|
153
|
+
"""
|
|
154
|
+
normalized = self.normalize_error(error)
|
|
155
|
+
return hashlib.sha256(normalized.encode()).hexdigest()[:12]
|
|
156
|
+
|
|
157
|
+
def extract_error_type(self, error: str) -> Optional[str]:
|
|
158
|
+
"""Extract the error type from an error message.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
error: Error message
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Error type like "ModuleNotFoundError" or None
|
|
165
|
+
"""
|
|
166
|
+
# Common Python error patterns
|
|
167
|
+
patterns = [
|
|
168
|
+
r'(\w+Error):',
|
|
169
|
+
r'(\w+Exception):',
|
|
170
|
+
r'(\w+Warning):',
|
|
171
|
+
r'^(E\d+)', # ruff/flake8 codes
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for pattern in patterns:
|
|
175
|
+
match = re.search(pattern, error, re.MULTILINE)
|
|
176
|
+
if match:
|
|
177
|
+
return match.group(1)
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def record_attempt(
|
|
182
|
+
self,
|
|
183
|
+
error: str,
|
|
184
|
+
fix_description: str,
|
|
185
|
+
file_path: Optional[str] = None,
|
|
186
|
+
) -> FixAttempt:
|
|
187
|
+
"""Record a fix attempt.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
error: The error being fixed
|
|
191
|
+
fix_description: Description of the fix being attempted
|
|
192
|
+
file_path: File being fixed (if applicable)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The recorded FixAttempt
|
|
196
|
+
"""
|
|
197
|
+
error_sig = self.hash_error(error)
|
|
198
|
+
error_type = self.extract_error_type(error)
|
|
199
|
+
|
|
200
|
+
attempt = FixAttempt(
|
|
201
|
+
error_signature=error_sig,
|
|
202
|
+
fix_description=fix_description,
|
|
203
|
+
outcome=FixOutcome.PENDING,
|
|
204
|
+
file_path=file_path,
|
|
205
|
+
error_type=error_type,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self._attempts.append(attempt)
|
|
209
|
+
return attempt
|
|
210
|
+
|
|
211
|
+
def record_outcome(
|
|
212
|
+
self,
|
|
213
|
+
error: str,
|
|
214
|
+
fix_description: str,
|
|
215
|
+
outcome: FixOutcome,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Record the outcome of a fix attempt.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
error: The error that was being fixed
|
|
221
|
+
fix_description: Description of the fix
|
|
222
|
+
outcome: Whether it succeeded or failed
|
|
223
|
+
"""
|
|
224
|
+
error_sig = self.hash_error(error)
|
|
225
|
+
|
|
226
|
+
# Find the matching attempt and update it
|
|
227
|
+
for attempt in reversed(self._attempts):
|
|
228
|
+
if (attempt.error_signature == error_sig and
|
|
229
|
+
attempt.fix_description == fix_description and
|
|
230
|
+
attempt.outcome == FixOutcome.PENDING):
|
|
231
|
+
attempt.outcome = outcome
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
# Update counts
|
|
235
|
+
if outcome == FixOutcome.FAILED:
|
|
236
|
+
self._error_counts[error_sig] = self._error_counts.get(error_sig, 0) + 1
|
|
237
|
+
|
|
238
|
+
# Also track file-level failures
|
|
239
|
+
for attempt in reversed(self._attempts):
|
|
240
|
+
if (attempt.error_signature == error_sig and
|
|
241
|
+
attempt.file_path):
|
|
242
|
+
self._file_counts[attempt.file_path] = \
|
|
243
|
+
self._file_counts.get(attempt.file_path, 0) + 1
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
def was_attempted(self, error: str, fix_description: str) -> bool:
|
|
247
|
+
"""Check if a specific fix was already attempted for an error.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
error: The error message
|
|
251
|
+
fix_description: The fix to check
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if this fix was already tried for this error
|
|
255
|
+
"""
|
|
256
|
+
error_sig = self.hash_error(error)
|
|
257
|
+
|
|
258
|
+
for attempt in self._attempts:
|
|
259
|
+
if (attempt.error_signature == error_sig and
|
|
260
|
+
attempt.fix_description.lower() == fix_description.lower()):
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
def get_attempted_fixes(self, error: str) -> list[str]:
|
|
266
|
+
"""Get list of fixes already attempted for an error.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
error: The error message
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of fix descriptions that were tried
|
|
273
|
+
"""
|
|
274
|
+
error_sig = self.hash_error(error)
|
|
275
|
+
return [
|
|
276
|
+
a.fix_description
|
|
277
|
+
for a in self._attempts
|
|
278
|
+
if a.error_signature == error_sig
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
def get_failure_count(self, error: str) -> int:
|
|
282
|
+
"""Get number of failed fix attempts for an error.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
error: The error message
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Number of failed attempts
|
|
289
|
+
"""
|
|
290
|
+
error_sig = self.hash_error(error)
|
|
291
|
+
return self._error_counts.get(error_sig, 0)
|
|
292
|
+
|
|
293
|
+
def get_file_failure_count(self, file_path: str) -> int:
|
|
294
|
+
"""Get number of failures for a specific file.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
file_path: Path to the file
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Number of failed attempts for this file
|
|
301
|
+
"""
|
|
302
|
+
return self._file_counts.get(file_path, 0)
|
|
303
|
+
|
|
304
|
+
def get_total_failures(self) -> int:
|
|
305
|
+
"""Get total number of failed fix attempts.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Total failure count across all errors
|
|
309
|
+
"""
|
|
310
|
+
return sum(self._error_counts.values())
|
|
311
|
+
|
|
312
|
+
def should_escalate(self, error: str, file_path: Optional[str] = None) -> EscalationDecision:
|
|
313
|
+
"""Determine if we should escalate to a blocker.
|
|
314
|
+
|
|
315
|
+
Escalation rules:
|
|
316
|
+
1. Same error type fails 3+ times → blocker
|
|
317
|
+
2. Same file fails 3+ times with different errors → blocker
|
|
318
|
+
3. Total failures in run > 5 → blocker
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
error: Current error message
|
|
322
|
+
file_path: File being worked on (if applicable)
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
EscalationDecision with recommendation and context
|
|
326
|
+
"""
|
|
327
|
+
error_sig = self.hash_error(error)
|
|
328
|
+
error_count = self._error_counts.get(error_sig, 0)
|
|
329
|
+
total_failures = self.get_total_failures()
|
|
330
|
+
attempted = self.get_attempted_fixes(error)
|
|
331
|
+
|
|
332
|
+
# Rule 1: Same error fails too many times
|
|
333
|
+
if error_count >= MAX_SAME_ERROR_ATTEMPTS:
|
|
334
|
+
error_type = self.extract_error_type(error) or "error"
|
|
335
|
+
return EscalationDecision(
|
|
336
|
+
should_escalate=True,
|
|
337
|
+
reason=f"Same {error_type} has failed {error_count} times despite fixes",
|
|
338
|
+
attempted_fixes=attempted,
|
|
339
|
+
error_summary=self.normalize_error(error)[:200],
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Rule 2: Same file keeps failing
|
|
343
|
+
if file_path:
|
|
344
|
+
file_count = self._file_counts.get(file_path, 0)
|
|
345
|
+
if file_count >= MAX_SAME_FILE_ATTEMPTS:
|
|
346
|
+
return EscalationDecision(
|
|
347
|
+
should_escalate=True,
|
|
348
|
+
reason=f"File '{file_path}' has failed {file_count} times with various errors",
|
|
349
|
+
attempted_fixes=attempted,
|
|
350
|
+
error_summary=f"Multiple errors in {file_path}",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Rule 3: Too many total failures
|
|
354
|
+
if total_failures >= MAX_TOTAL_FAILURES:
|
|
355
|
+
return EscalationDecision(
|
|
356
|
+
should_escalate=True,
|
|
357
|
+
reason=f"Total of {total_failures} failures in this run exceeds threshold",
|
|
358
|
+
attempted_fixes=attempted,
|
|
359
|
+
error_summary="Multiple errors across the task",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# No escalation needed
|
|
363
|
+
return EscalationDecision(
|
|
364
|
+
should_escalate=False,
|
|
365
|
+
reason="Within acceptable failure limits",
|
|
366
|
+
attempted_fixes=attempted,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def get_blocker_context(self, error: str) -> dict:
|
|
370
|
+
"""Generate context for creating an informative blocker.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
error: Current error message
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dictionary with blocker context
|
|
377
|
+
"""
|
|
378
|
+
error_sig = self.hash_error(error)
|
|
379
|
+
error_type = self.extract_error_type(error)
|
|
380
|
+
attempted = self.get_attempted_fixes(error)
|
|
381
|
+
|
|
382
|
+
# Find all files affected by this error
|
|
383
|
+
affected_files = set()
|
|
384
|
+
for attempt in self._attempts:
|
|
385
|
+
if attempt.error_signature == error_sig and attempt.file_path:
|
|
386
|
+
affected_files.add(attempt.file_path)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"error_type": error_type,
|
|
390
|
+
"error_signature": error_sig,
|
|
391
|
+
"attempt_count": self._error_counts.get(error_sig, 0),
|
|
392
|
+
"attempted_fixes": attempted,
|
|
393
|
+
"affected_files": list(affected_files),
|
|
394
|
+
"total_run_failures": self.get_total_failures(),
|
|
395
|
+
"normalized_error": self.normalize_error(error),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
def reset(self) -> None:
|
|
399
|
+
"""Reset all tracking state.
|
|
400
|
+
|
|
401
|
+
Call this when starting a new task or run.
|
|
402
|
+
"""
|
|
403
|
+
self._attempts.clear()
|
|
404
|
+
self._error_counts.clear()
|
|
405
|
+
self._file_counts.clear()
|
|
406
|
+
|
|
407
|
+
def to_dict(self) -> dict:
|
|
408
|
+
"""Serialize tracker state for persistence.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Dictionary representation of tracker state
|
|
412
|
+
"""
|
|
413
|
+
return {
|
|
414
|
+
"attempts": [
|
|
415
|
+
{
|
|
416
|
+
"error_signature": a.error_signature,
|
|
417
|
+
"fix_description": a.fix_description,
|
|
418
|
+
"outcome": a.outcome.value,
|
|
419
|
+
"timestamp": a.timestamp.isoformat(),
|
|
420
|
+
"file_path": a.file_path,
|
|
421
|
+
"error_type": a.error_type,
|
|
422
|
+
}
|
|
423
|
+
for a in self._attempts
|
|
424
|
+
],
|
|
425
|
+
"error_counts": dict(self._error_counts),
|
|
426
|
+
"file_counts": dict(self._file_counts),
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
def from_dict(cls, data: dict) -> "FixAttemptTracker":
|
|
431
|
+
"""Restore tracker state from persistence.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
data: Dictionary from to_dict()
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Restored FixAttemptTracker
|
|
438
|
+
"""
|
|
439
|
+
tracker = cls()
|
|
440
|
+
|
|
441
|
+
for a in data.get("attempts", []):
|
|
442
|
+
attempt = FixAttempt(
|
|
443
|
+
error_signature=a["error_signature"],
|
|
444
|
+
fix_description=a["fix_description"],
|
|
445
|
+
outcome=FixOutcome(a["outcome"]),
|
|
446
|
+
timestamp=datetime.fromisoformat(a["timestamp"]),
|
|
447
|
+
file_path=a.get("file_path"),
|
|
448
|
+
error_type=a.get("error_type"),
|
|
449
|
+
)
|
|
450
|
+
tracker._attempts.append(attempt)
|
|
451
|
+
|
|
452
|
+
tracker._error_counts = dict(data.get("error_counts", {}))
|
|
453
|
+
tracker._file_counts = dict(data.get("file_counts", {}))
|
|
454
|
+
|
|
455
|
+
return tracker
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def build_escalation_question(
|
|
459
|
+
error: str,
|
|
460
|
+
escalation_reason: str,
|
|
461
|
+
fix_tracker: FixAttemptTracker,
|
|
462
|
+
) -> str:
|
|
463
|
+
"""Build a human-readable blocker question for escalation.
|
|
464
|
+
|
|
465
|
+
Shared by VerificationWrapper and ReactAgent to produce consistent
|
|
466
|
+
escalation blocker messages.
|
|
467
|
+
"""
|
|
468
|
+
context = fix_tracker.get_blocker_context(error)
|
|
469
|
+
attempted = context.get("attempted_fixes", [])
|
|
470
|
+
attempted_str = (
|
|
471
|
+
"\n".join(f" - {f}" for f in attempted) if attempted else " (none)"
|
|
472
|
+
)
|
|
473
|
+
return (
|
|
474
|
+
f"Verification keeps failing and automated fixes are not working.\n\n"
|
|
475
|
+
f"Error: {error[:300]}\n\n"
|
|
476
|
+
f"Reason for escalation: {escalation_reason}\n\n"
|
|
477
|
+
f"Fixes already attempted:\n{attempted_str}\n\n"
|
|
478
|
+
f"Total failures in this run: {context.get('total_run_failures', 0)}\n\n"
|
|
479
|
+
f"Please investigate and provide guidance."
|
|
480
|
+
)
|