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,712 @@
|
|
|
1
|
+
"""Diagnostic Agent for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
Analyzes failed runs and generates actionable recommendations.
|
|
4
|
+
|
|
5
|
+
This module provides:
|
|
6
|
+
- Pattern-based failure detection
|
|
7
|
+
- LLM-powered root cause analysis
|
|
8
|
+
- Recommendation generation
|
|
9
|
+
- Severity assessment
|
|
10
|
+
|
|
11
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import TYPE_CHECKING, Optional
|
|
17
|
+
|
|
18
|
+
from codeframe.adapters.llm import Purpose
|
|
19
|
+
from codeframe.core.diagnostics import (
|
|
20
|
+
DiagnosticRecommendation,
|
|
21
|
+
DiagnosticReport,
|
|
22
|
+
FailureCategory,
|
|
23
|
+
LogCategory,
|
|
24
|
+
LogLevel,
|
|
25
|
+
RemediationAction,
|
|
26
|
+
RunLogEntry,
|
|
27
|
+
Severity,
|
|
28
|
+
get_run_errors,
|
|
29
|
+
get_run_logs,
|
|
30
|
+
save_diagnostic_report,
|
|
31
|
+
)
|
|
32
|
+
from codeframe.core.workspace import Workspace
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from codeframe.adapters.llm.base import LLMProvider
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _utc_now() -> datetime:
|
|
39
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
40
|
+
return datetime.now(timezone.utc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Pattern Definitions for Failure Detection
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
# Patterns for task description issues
|
|
48
|
+
TASK_DESCRIPTION_PATTERNS = [
|
|
49
|
+
r"ambiguous",
|
|
50
|
+
r"unclear",
|
|
51
|
+
r"vague",
|
|
52
|
+
r"unspecified",
|
|
53
|
+
r"missing.*requirement",
|
|
54
|
+
r"cannot.*determine",
|
|
55
|
+
r"lack.*acceptance.*criteria",
|
|
56
|
+
r"need.*clarification",
|
|
57
|
+
r"requirements.*incomplete",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Patterns for model/API limitations
|
|
61
|
+
MODEL_LIMITATION_PATTERNS = [
|
|
62
|
+
r"context.*length.*exceeded",
|
|
63
|
+
r"token.*limit",
|
|
64
|
+
r"maximum.*tokens",
|
|
65
|
+
r"rate.*limit",
|
|
66
|
+
r"api.*error.*429",
|
|
67
|
+
r"quota.*exceeded",
|
|
68
|
+
r"model.*unavailable",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Patterns for dependency issues
|
|
72
|
+
DEPENDENCY_PATTERNS = [
|
|
73
|
+
r"modulenotfounderror",
|
|
74
|
+
r"importerror",
|
|
75
|
+
r"no module named",
|
|
76
|
+
r"package.*not.*found",
|
|
77
|
+
r"dependency.*missing",
|
|
78
|
+
r"cannot.*import",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Patterns for code quality issues
|
|
82
|
+
CODE_QUALITY_PATTERNS = [
|
|
83
|
+
r"test.*failed",
|
|
84
|
+
r"pytest.*failed",
|
|
85
|
+
r"lint.*error",
|
|
86
|
+
r"type.*error",
|
|
87
|
+
r"ruff.*check.*failed",
|
|
88
|
+
r"mypy.*error",
|
|
89
|
+
r"assertion.*failed",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Patterns for environment issues
|
|
93
|
+
ENVIRONMENT_PATTERNS = [
|
|
94
|
+
r"permission.*denied",
|
|
95
|
+
r"file.*not.*found",
|
|
96
|
+
r"directory.*not.*exist",
|
|
97
|
+
r"environment.*variable.*not.*set",
|
|
98
|
+
r"command.*not.*found",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Patterns for blocker-related issues
|
|
102
|
+
BLOCKER_PATTERNS = [
|
|
103
|
+
r"blocker.*created",
|
|
104
|
+
r"human.*input.*needed",
|
|
105
|
+
r"waiting.*for.*answer",
|
|
106
|
+
r"escalated.*to.*human",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# Pattern Detection Functions
|
|
112
|
+
# =============================================================================
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detect_failure_patterns(logs: list[RunLogEntry]) -> set[FailureCategory]:
|
|
116
|
+
"""Detect failure patterns from log entries.
|
|
117
|
+
|
|
118
|
+
Analyzes log messages to identify likely failure categories
|
|
119
|
+
without using the LLM.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
logs: List of log entries to analyze
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Set of detected failure categories
|
|
126
|
+
"""
|
|
127
|
+
detected: set[FailureCategory] = set()
|
|
128
|
+
|
|
129
|
+
# Combine all messages for pattern matching
|
|
130
|
+
all_text = " ".join(
|
|
131
|
+
f"{log.message} {log.metadata}" if log.metadata else log.message
|
|
132
|
+
for log in logs
|
|
133
|
+
).lower()
|
|
134
|
+
|
|
135
|
+
# Check each pattern category
|
|
136
|
+
for pattern in TASK_DESCRIPTION_PATTERNS:
|
|
137
|
+
if re.search(pattern, all_text):
|
|
138
|
+
detected.add(FailureCategory.TASK_DESCRIPTION)
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
for pattern in MODEL_LIMITATION_PATTERNS:
|
|
142
|
+
if re.search(pattern, all_text):
|
|
143
|
+
detected.add(FailureCategory.MODEL_LIMITATION)
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
for pattern in DEPENDENCY_PATTERNS:
|
|
147
|
+
if re.search(pattern, all_text):
|
|
148
|
+
detected.add(FailureCategory.DEPENDENCY_ISSUE)
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
for pattern in CODE_QUALITY_PATTERNS:
|
|
152
|
+
if re.search(pattern, all_text):
|
|
153
|
+
detected.add(FailureCategory.CODE_QUALITY)
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
for pattern in ENVIRONMENT_PATTERNS:
|
|
157
|
+
if re.search(pattern, all_text):
|
|
158
|
+
detected.add(FailureCategory.ENVIRONMENT_ISSUE)
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
for pattern in BLOCKER_PATTERNS:
|
|
162
|
+
if re.search(pattern, all_text):
|
|
163
|
+
detected.add(FailureCategory.BLOCKER_UNRESOLVED)
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
return detected
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def detect_primary_failure_category(logs: list[RunLogEntry]) -> FailureCategory:
|
|
170
|
+
"""Detect the primary failure category from logs.
|
|
171
|
+
|
|
172
|
+
Returns the most likely failure category, prioritizing
|
|
173
|
+
more actionable categories.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
logs: List of log entries to analyze
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Primary failure category
|
|
180
|
+
"""
|
|
181
|
+
detected = detect_failure_patterns(logs)
|
|
182
|
+
|
|
183
|
+
# Priority order (most actionable first)
|
|
184
|
+
priority = [
|
|
185
|
+
FailureCategory.TASK_DESCRIPTION,
|
|
186
|
+
FailureCategory.BLOCKER_UNRESOLVED,
|
|
187
|
+
FailureCategory.DEPENDENCY_ISSUE,
|
|
188
|
+
FailureCategory.ENVIRONMENT_ISSUE,
|
|
189
|
+
FailureCategory.CODE_QUALITY,
|
|
190
|
+
FailureCategory.MODEL_LIMITATION,
|
|
191
|
+
FailureCategory.TECHNICAL_ERROR,
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for category in priority:
|
|
195
|
+
if category in detected:
|
|
196
|
+
return category
|
|
197
|
+
|
|
198
|
+
return FailureCategory.UNKNOWN
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# =============================================================================
|
|
202
|
+
# Recommendation Generation
|
|
203
|
+
# =============================================================================
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def generate_recommendations(
|
|
207
|
+
task_id: str,
|
|
208
|
+
run_id: str,
|
|
209
|
+
failure_category: FailureCategory,
|
|
210
|
+
error_messages: list[str],
|
|
211
|
+
) -> list[DiagnosticRecommendation]:
|
|
212
|
+
"""Generate remediation recommendations based on failure category.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
task_id: ID of the failed task
|
|
216
|
+
run_id: ID of the failed run
|
|
217
|
+
failure_category: Detected failure category
|
|
218
|
+
error_messages: List of error messages from logs
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of actionable recommendations
|
|
222
|
+
"""
|
|
223
|
+
recommendations: list[DiagnosticRecommendation] = []
|
|
224
|
+
|
|
225
|
+
if failure_category == FailureCategory.TASK_DESCRIPTION:
|
|
226
|
+
recommendations.append(
|
|
227
|
+
DiagnosticRecommendation(
|
|
228
|
+
action=RemediationAction.UPDATE_TASK_DESCRIPTION,
|
|
229
|
+
reason="Task description may be ambiguous or incomplete. Adding clearer acceptance criteria will help the agent understand requirements.",
|
|
230
|
+
command=f"cf work update-description {task_id} \"<new description with clear acceptance criteria>\"",
|
|
231
|
+
parameters={"task_id": task_id},
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
recommendations.append(
|
|
235
|
+
DiagnosticRecommendation(
|
|
236
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
237
|
+
reason="After updating the description, retry the task",
|
|
238
|
+
command=f"cf work retry {task_id}",
|
|
239
|
+
parameters={"task_id": task_id},
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
elif failure_category == FailureCategory.BLOCKER_UNRESOLVED:
|
|
244
|
+
recommendations.append(
|
|
245
|
+
DiagnosticRecommendation(
|
|
246
|
+
action=RemediationAction.ANSWER_BLOCKER,
|
|
247
|
+
reason="The task has an unresolved blocker that needs human input",
|
|
248
|
+
command="cf blocker list # Find and answer the open blocker",
|
|
249
|
+
parameters={"task_id": task_id},
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
elif failure_category == FailureCategory.DEPENDENCY_ISSUE:
|
|
254
|
+
# Try to extract package name from error messages
|
|
255
|
+
package_name = _extract_package_name(error_messages)
|
|
256
|
+
if package_name:
|
|
257
|
+
recommendations.append(
|
|
258
|
+
DiagnosticRecommendation(
|
|
259
|
+
action=RemediationAction.RESOLVE_DEPENDENCY,
|
|
260
|
+
reason=f"Missing package: {package_name}. Install it and retry.",
|
|
261
|
+
command=f"uv pip install {package_name} # or: pip install {package_name}",
|
|
262
|
+
parameters={"package": package_name},
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
recommendations.append(
|
|
267
|
+
DiagnosticRecommendation(
|
|
268
|
+
action=RemediationAction.RESOLVE_DEPENDENCY,
|
|
269
|
+
reason="Missing dependencies detected. Review the error and install required packages.",
|
|
270
|
+
command="# Review error messages and install missing dependencies",
|
|
271
|
+
parameters={},
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
recommendations.append(
|
|
275
|
+
DiagnosticRecommendation(
|
|
276
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
277
|
+
reason="After resolving dependencies, retry the task",
|
|
278
|
+
command=f"cf work retry {task_id}",
|
|
279
|
+
parameters={"task_id": task_id},
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
elif failure_category == FailureCategory.ENVIRONMENT_ISSUE:
|
|
284
|
+
recommendations.append(
|
|
285
|
+
DiagnosticRecommendation(
|
|
286
|
+
action=RemediationAction.FIX_ENVIRONMENT,
|
|
287
|
+
reason="Environment configuration issue detected. Check file permissions, paths, and environment variables.",
|
|
288
|
+
command="# Review error messages and fix environment configuration",
|
|
289
|
+
parameters={},
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
recommendations.append(
|
|
293
|
+
DiagnosticRecommendation(
|
|
294
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
295
|
+
reason="After fixing the environment, retry the task",
|
|
296
|
+
command=f"cf work retry {task_id}",
|
|
297
|
+
parameters={"task_id": task_id},
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
elif failure_category == FailureCategory.CODE_QUALITY:
|
|
302
|
+
recommendations.append(
|
|
303
|
+
DiagnosticRecommendation(
|
|
304
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
305
|
+
reason="Code quality issues (tests/lint) detected. The agent will try to self-correct on retry.",
|
|
306
|
+
command=f"cf work retry {task_id} --verbose",
|
|
307
|
+
parameters={"task_id": task_id, "verbose": True},
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
elif failure_category == FailureCategory.MODEL_LIMITATION:
|
|
312
|
+
recommendations.append(
|
|
313
|
+
DiagnosticRecommendation(
|
|
314
|
+
action=RemediationAction.CHANGE_MODEL,
|
|
315
|
+
reason="Model limitation detected (token limit, rate limit, etc.). Consider using a different model or breaking down the task.",
|
|
316
|
+
command="# Consider splitting task or using a model with larger context",
|
|
317
|
+
parameters={"task_id": task_id},
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
recommendations.append(
|
|
321
|
+
DiagnosticRecommendation(
|
|
322
|
+
action=RemediationAction.SPLIT_TASK,
|
|
323
|
+
reason="The task may be too large. Consider splitting it into smaller subtasks.",
|
|
324
|
+
command="# Review task and consider breaking into smaller pieces",
|
|
325
|
+
parameters={"task_id": task_id},
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
elif failure_category == FailureCategory.TECHNICAL_ERROR:
|
|
330
|
+
recommendations.append(
|
|
331
|
+
DiagnosticRecommendation(
|
|
332
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
333
|
+
reason="Technical error occurred. A simple retry may resolve transient issues.",
|
|
334
|
+
command=f"cf work retry {task_id}",
|
|
335
|
+
parameters={"task_id": task_id},
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
else: # UNKNOWN
|
|
340
|
+
recommendations.append(
|
|
341
|
+
DiagnosticRecommendation(
|
|
342
|
+
action=RemediationAction.RETRY_WITH_CONTEXT,
|
|
343
|
+
reason="Unable to determine specific failure cause. Try running with verbose mode for more details.",
|
|
344
|
+
command=f"cf work retry {task_id} --verbose",
|
|
345
|
+
parameters={"task_id": task_id, "verbose": True},
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return recommendations
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _extract_package_name(error_messages: list[str]) -> Optional[str]:
|
|
353
|
+
"""Extract package name from error messages.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
error_messages: List of error messages
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Package name if found, None otherwise
|
|
360
|
+
"""
|
|
361
|
+
for msg in error_messages:
|
|
362
|
+
# ModuleNotFoundError: No module named 'package_name'
|
|
363
|
+
# Handles: simple names, hyphenated (pydantic-core), dotted (google.auth)
|
|
364
|
+
match = re.search(r"no module named ['\"]?([\w\-\.]+)", msg, re.IGNORECASE)
|
|
365
|
+
if match:
|
|
366
|
+
return match.group(1)
|
|
367
|
+
|
|
368
|
+
# ImportError: cannot import name 'x' from 'package'
|
|
369
|
+
# Handles: simple names, hyphenated (typing-extensions), dotted (google.auth)
|
|
370
|
+
match = re.search(r"cannot import.*from ['\"]?([\w\-\.]+)", msg, re.IGNORECASE)
|
|
371
|
+
if match:
|
|
372
|
+
return match.group(1)
|
|
373
|
+
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# =============================================================================
|
|
378
|
+
# Severity Assessment
|
|
379
|
+
# =============================================================================
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def assess_severity(
|
|
383
|
+
failure_category: FailureCategory,
|
|
384
|
+
error_count: int,
|
|
385
|
+
has_blocker: bool,
|
|
386
|
+
) -> Severity:
|
|
387
|
+
"""Assess the severity of a failure.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
failure_category: The detected failure category
|
|
391
|
+
error_count: Number of error log entries
|
|
392
|
+
has_blocker: Whether a blocker was created
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Severity level
|
|
396
|
+
"""
|
|
397
|
+
# Critical: Many errors or blockers
|
|
398
|
+
if error_count >= 5 and has_blocker:
|
|
399
|
+
return Severity.CRITICAL
|
|
400
|
+
|
|
401
|
+
# High: Task description issues need attention
|
|
402
|
+
if failure_category == FailureCategory.TASK_DESCRIPTION:
|
|
403
|
+
return Severity.HIGH
|
|
404
|
+
|
|
405
|
+
# High: Unresolved blockers
|
|
406
|
+
if failure_category == FailureCategory.BLOCKER_UNRESOLVED:
|
|
407
|
+
return Severity.HIGH
|
|
408
|
+
|
|
409
|
+
# Medium: Code quality or environment issues
|
|
410
|
+
if failure_category in (
|
|
411
|
+
FailureCategory.CODE_QUALITY,
|
|
412
|
+
FailureCategory.DEPENDENCY_ISSUE,
|
|
413
|
+
FailureCategory.ENVIRONMENT_ISSUE,
|
|
414
|
+
):
|
|
415
|
+
return Severity.MEDIUM
|
|
416
|
+
|
|
417
|
+
# Medium: Multiple errors
|
|
418
|
+
if error_count >= 3:
|
|
419
|
+
return Severity.MEDIUM
|
|
420
|
+
|
|
421
|
+
# Low: Single transient errors
|
|
422
|
+
return Severity.LOW
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# =============================================================================
|
|
426
|
+
# Log Summarization
|
|
427
|
+
# =============================================================================
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def summarize_logs(logs: list[RunLogEntry], max_length: int = 1000) -> str:
|
|
431
|
+
"""Summarize log entries into a human-readable format.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
logs: List of log entries to summarize
|
|
435
|
+
max_length: Maximum length of summary
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Human-readable summary string
|
|
439
|
+
"""
|
|
440
|
+
if not logs:
|
|
441
|
+
return "No log entries found for this run."
|
|
442
|
+
|
|
443
|
+
# Focus on errors and important events
|
|
444
|
+
important_logs = [
|
|
445
|
+
log for log in logs
|
|
446
|
+
if log.log_level in (LogLevel.ERROR, LogLevel.WARNING)
|
|
447
|
+
or log.category in (LogCategory.AGENT_ACTION, LogCategory.BLOCKER)
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
# If no important logs, use all logs
|
|
451
|
+
if not important_logs:
|
|
452
|
+
important_logs = logs
|
|
453
|
+
|
|
454
|
+
# Build summary
|
|
455
|
+
lines = []
|
|
456
|
+
for log in important_logs[:20]: # Limit to 20 entries
|
|
457
|
+
prefix = "ERROR" if log.log_level == LogLevel.ERROR else log.log_level.value
|
|
458
|
+
lines.append(f"[{prefix}] {log.category.value}: {log.message[:200]}")
|
|
459
|
+
|
|
460
|
+
summary = "\n".join(lines)
|
|
461
|
+
|
|
462
|
+
# Truncate if necessary (guard against negative index when max_length < 3)
|
|
463
|
+
if len(summary) > max_length:
|
|
464
|
+
summary = summary[:max(0, max_length - 3)] + "..."
|
|
465
|
+
|
|
466
|
+
return summary
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# =============================================================================
|
|
470
|
+
# Diagnostic Agent
|
|
471
|
+
# =============================================================================
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class DiagnosticAgent:
|
|
475
|
+
"""Agent that analyzes failed runs and generates diagnostic reports.
|
|
476
|
+
|
|
477
|
+
Uses pattern matching for quick detection and optionally uses
|
|
478
|
+
LLM for deeper analysis of complex failures.
|
|
479
|
+
|
|
480
|
+
Usage:
|
|
481
|
+
agent = DiagnosticAgent(workspace, llm_provider)
|
|
482
|
+
report = agent.analyze(task_id, run_id)
|
|
483
|
+
# report contains root cause, recommendations, etc.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
def __init__(
|
|
487
|
+
self,
|
|
488
|
+
workspace: Workspace,
|
|
489
|
+
llm_provider: Optional["LLMProvider"] = None,
|
|
490
|
+
):
|
|
491
|
+
"""Initialize the diagnostic agent.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
workspace: Target workspace
|
|
495
|
+
llm_provider: Optional LLM provider for deep analysis
|
|
496
|
+
"""
|
|
497
|
+
self.workspace = workspace
|
|
498
|
+
self.llm_provider = llm_provider
|
|
499
|
+
|
|
500
|
+
def analyze(self, task_id: str, run_id: str) -> DiagnosticReport:
|
|
501
|
+
"""Analyze a failed run and generate a diagnostic report.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
task_id: ID of the failed task
|
|
505
|
+
run_id: ID of the failed run
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
DiagnosticReport with analysis and recommendations
|
|
509
|
+
"""
|
|
510
|
+
# Get logs for analysis
|
|
511
|
+
logs = get_run_logs(self.workspace, run_id)
|
|
512
|
+
errors = get_run_errors(self.workspace, run_id)
|
|
513
|
+
|
|
514
|
+
# Detect failure category from patterns
|
|
515
|
+
failure_category = detect_primary_failure_category(logs)
|
|
516
|
+
|
|
517
|
+
# Extract error messages
|
|
518
|
+
error_messages = [e.message for e in errors]
|
|
519
|
+
|
|
520
|
+
# Check for blockers
|
|
521
|
+
has_blocker = any(
|
|
522
|
+
log.category == LogCategory.BLOCKER
|
|
523
|
+
for log in logs
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Generate log summary
|
|
527
|
+
log_summary = summarize_logs(logs)
|
|
528
|
+
|
|
529
|
+
# Determine root cause (may update failure_category via LLM)
|
|
530
|
+
if self.llm_provider and logs:
|
|
531
|
+
root_cause = self._analyze_with_llm(logs, error_messages)
|
|
532
|
+
# Parse LLM response to potentially update category
|
|
533
|
+
llm_category = self._extract_category_from_llm(root_cause)
|
|
534
|
+
if llm_category != FailureCategory.UNKNOWN:
|
|
535
|
+
failure_category = llm_category
|
|
536
|
+
else:
|
|
537
|
+
root_cause = self._generate_root_cause(failure_category, error_messages)
|
|
538
|
+
|
|
539
|
+
# Assess severity AFTER potential LLM category update
|
|
540
|
+
severity = assess_severity(
|
|
541
|
+
failure_category=failure_category,
|
|
542
|
+
error_count=len(errors),
|
|
543
|
+
has_blocker=has_blocker,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Generate recommendations AFTER potential LLM category update
|
|
547
|
+
recommendations = generate_recommendations(
|
|
548
|
+
task_id=task_id,
|
|
549
|
+
run_id=run_id,
|
|
550
|
+
failure_category=failure_category,
|
|
551
|
+
error_messages=error_messages,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Create report
|
|
555
|
+
report = DiagnosticReport(
|
|
556
|
+
task_id=task_id,
|
|
557
|
+
run_id=run_id,
|
|
558
|
+
root_cause=root_cause,
|
|
559
|
+
failure_category=failure_category,
|
|
560
|
+
severity=severity,
|
|
561
|
+
recommendations=recommendations,
|
|
562
|
+
log_summary=log_summary,
|
|
563
|
+
created_at=_utc_now(),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Save to database
|
|
567
|
+
save_diagnostic_report(self.workspace, report)
|
|
568
|
+
|
|
569
|
+
return report
|
|
570
|
+
|
|
571
|
+
def _analyze_with_llm(
|
|
572
|
+
self,
|
|
573
|
+
logs: list[RunLogEntry],
|
|
574
|
+
error_messages: list[str],
|
|
575
|
+
) -> str:
|
|
576
|
+
"""Use LLM to analyze logs and determine root cause.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
logs: Log entries to analyze
|
|
580
|
+
error_messages: Extracted error messages
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Root cause description from LLM
|
|
584
|
+
"""
|
|
585
|
+
if not self.llm_provider:
|
|
586
|
+
return "LLM analysis not available"
|
|
587
|
+
|
|
588
|
+
# Build prompt
|
|
589
|
+
log_text = "\n".join(
|
|
590
|
+
f"[{log.log_level.value}] {log.category.value}: {log.message}"
|
|
591
|
+
for log in logs[-30:] # Last 30 logs
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
prompt = f"""Analyze the following agent execution logs and determine the root cause of the failure.
|
|
595
|
+
|
|
596
|
+
Logs:
|
|
597
|
+
{log_text}
|
|
598
|
+
|
|
599
|
+
Error messages:
|
|
600
|
+
{chr(10).join(error_messages[:10])}
|
|
601
|
+
|
|
602
|
+
Provide your analysis in this format:
|
|
603
|
+
Root Cause: [One sentence description of the root cause]
|
|
604
|
+
Failure Category: [One of: task_description, blocker_unresolved, model_limitation, code_quality, dependency_issue, environment_issue, technical_error, unknown]
|
|
605
|
+
Severity: [One of: critical, high, medium, low]
|
|
606
|
+
|
|
607
|
+
Then provide recommendations."""
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
response = self.llm_provider.complete(
|
|
611
|
+
messages=[{"role": "user", "content": prompt}],
|
|
612
|
+
purpose=Purpose.GENERATION,
|
|
613
|
+
)
|
|
614
|
+
return response.content if hasattr(response, 'content') else str(response)
|
|
615
|
+
except Exception as e:
|
|
616
|
+
return f"LLM analysis failed: {e}"
|
|
617
|
+
|
|
618
|
+
def _extract_category_from_llm(self, llm_response: str) -> FailureCategory:
|
|
619
|
+
"""Extract failure category from LLM response.
|
|
620
|
+
|
|
621
|
+
Parses specifically the 'Failure Category:' line to avoid false matches
|
|
622
|
+
from category names appearing in other contexts (e.g., 'NOT a dependency_issue').
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
llm_response: Response from LLM
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Extracted failure category
|
|
629
|
+
"""
|
|
630
|
+
category_map = {
|
|
631
|
+
"task_description": FailureCategory.TASK_DESCRIPTION,
|
|
632
|
+
"blocker_unresolved": FailureCategory.BLOCKER_UNRESOLVED,
|
|
633
|
+
"model_limitation": FailureCategory.MODEL_LIMITATION,
|
|
634
|
+
"code_quality": FailureCategory.CODE_QUALITY,
|
|
635
|
+
"dependency_issue": FailureCategory.DEPENDENCY_ISSUE,
|
|
636
|
+
"environment_issue": FailureCategory.ENVIRONMENT_ISSUE,
|
|
637
|
+
"technical_error": FailureCategory.TECHNICAL_ERROR,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# Parse specifically the "Failure Category:" line
|
|
641
|
+
match = re.search(
|
|
642
|
+
r"failure\s*category\s*:\s*(\w+)",
|
|
643
|
+
llm_response,
|
|
644
|
+
re.IGNORECASE,
|
|
645
|
+
)
|
|
646
|
+
if match:
|
|
647
|
+
category_value = match.group(1).lower()
|
|
648
|
+
if category_value in category_map:
|
|
649
|
+
return category_map[category_value]
|
|
650
|
+
|
|
651
|
+
return FailureCategory.UNKNOWN
|
|
652
|
+
|
|
653
|
+
def _generate_root_cause(
|
|
654
|
+
self,
|
|
655
|
+
failure_category: FailureCategory,
|
|
656
|
+
error_messages: list[str],
|
|
657
|
+
) -> str:
|
|
658
|
+
"""Generate a root cause description without LLM.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
failure_category: Detected failure category
|
|
662
|
+
error_messages: Error messages from logs
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Root cause description
|
|
666
|
+
"""
|
|
667
|
+
category_descriptions = {
|
|
668
|
+
FailureCategory.TASK_DESCRIPTION: (
|
|
669
|
+
"Task description lacks clear requirements or acceptance criteria. "
|
|
670
|
+
"The agent could not determine the expected implementation approach."
|
|
671
|
+
),
|
|
672
|
+
FailureCategory.BLOCKER_UNRESOLVED: (
|
|
673
|
+
"Task is blocked waiting for human input. "
|
|
674
|
+
"An unresolved blocker needs to be answered before the task can continue."
|
|
675
|
+
),
|
|
676
|
+
FailureCategory.MODEL_LIMITATION: (
|
|
677
|
+
"The task exceeded model limitations (token limit, rate limit, etc.). "
|
|
678
|
+
"Consider using a model with larger context or breaking down the task."
|
|
679
|
+
),
|
|
680
|
+
FailureCategory.CODE_QUALITY: (
|
|
681
|
+
"Code quality checks (tests or linting) failed. "
|
|
682
|
+
"The agent's self-correction loop was unable to resolve the issues."
|
|
683
|
+
),
|
|
684
|
+
FailureCategory.DEPENDENCY_ISSUE: (
|
|
685
|
+
"Missing dependencies prevented task execution. "
|
|
686
|
+
"Required packages need to be installed before retrying."
|
|
687
|
+
),
|
|
688
|
+
FailureCategory.ENVIRONMENT_ISSUE: (
|
|
689
|
+
"Environment configuration issues prevented task execution. "
|
|
690
|
+
"Check file permissions, paths, and environment variables."
|
|
691
|
+
),
|
|
692
|
+
FailureCategory.TECHNICAL_ERROR: (
|
|
693
|
+
"A technical error occurred during task execution. "
|
|
694
|
+
"This may be a transient issue that could resolve on retry."
|
|
695
|
+
),
|
|
696
|
+
FailureCategory.UNKNOWN: (
|
|
697
|
+
"Unable to determine the specific cause of failure. "
|
|
698
|
+
"Review the logs for more details."
|
|
699
|
+
),
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
base_cause = category_descriptions.get(
|
|
703
|
+
failure_category,
|
|
704
|
+
"Unable to determine root cause.",
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Append first error if available
|
|
708
|
+
if error_messages:
|
|
709
|
+
first_error = error_messages[0][:200]
|
|
710
|
+
base_cause += f"\n\nFirst error: {first_error}"
|
|
711
|
+
|
|
712
|
+
return base_cause
|