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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. 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