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,165 @@
1
+ """Rate limiting configuration for CodeFRAME API.
2
+
3
+ This module provides configuration for API rate limiting using slowapi.
4
+ It delegates to GlobalConfig in core/config.py as the single source of truth
5
+ for environment variable handling.
6
+
7
+ Environment Variables (via GlobalConfig):
8
+ RATE_LIMIT_ENABLED: Enable/disable rate limiting (default: true)
9
+ RATE_LIMIT_AUTH: Rate limit for authentication endpoints (default: 10/minute)
10
+ RATE_LIMIT_STANDARD: Rate limit for standard API endpoints (default: 100/minute)
11
+ RATE_LIMIT_AI: Rate limit for AI/expensive operations (default: 20/minute)
12
+ RATE_LIMIT_WEBSOCKET: Rate limit for WebSocket connections (default: 30/minute)
13
+ RATE_LIMIT_STORAGE: Storage backend - memory or redis (default: memory)
14
+ RATE_LIMIT_TRUSTED_PROXIES: Comma-separated trusted proxy IPs/CIDRs
15
+ REDIS_URL: Redis connection URL for distributed rate limiting (optional)
16
+ """
17
+
18
+ import ipaddress
19
+ import logging
20
+ from dataclasses import dataclass, field
21
+ from functools import lru_cache
22
+ from typing import Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class RateLimitConfig:
29
+ """Configuration for API rate limiting.
30
+
31
+ Attributes:
32
+ auth_limit: Rate limit for authentication endpoints
33
+ standard_limit: Rate limit for standard API endpoints
34
+ ai_limit: Rate limit for AI/expensive operations
35
+ websocket_limit: Rate limit for WebSocket connections
36
+ enabled: Whether rate limiting is enabled
37
+ storage: Storage backend ('memory' or 'redis')
38
+ redis_url: Redis connection URL for distributed rate limiting
39
+ trusted_proxies: List of trusted proxy IP addresses/networks
40
+ """
41
+
42
+ auth_limit: str = "10/minute"
43
+ standard_limit: str = "100/minute"
44
+ ai_limit: str = "20/minute"
45
+ websocket_limit: str = "30/minute"
46
+ enabled: bool = True
47
+ storage: str = "memory"
48
+ redis_url: Optional[str] = None
49
+ trusted_proxies: list = field(default_factory=list)
50
+
51
+ def is_trusted_proxy(self, ip: str) -> bool:
52
+ """Check if an IP address is from a trusted proxy.
53
+
54
+ Args:
55
+ ip: IP address to check
56
+
57
+ Returns:
58
+ True if IP is in trusted_proxies list or matches a trusted network
59
+ """
60
+ if not self.trusted_proxies:
61
+ return False
62
+
63
+ try:
64
+ client_ip = ipaddress.ip_address(ip)
65
+ for proxy in self.trusted_proxies:
66
+ try:
67
+ # Check if it's a network (CIDR notation)
68
+ if "/" in proxy:
69
+ network = ipaddress.ip_network(proxy, strict=False)
70
+ if client_ip in network:
71
+ return True
72
+ else:
73
+ # Check exact IP match
74
+ if client_ip == ipaddress.ip_address(proxy):
75
+ return True
76
+ except ValueError:
77
+ # Invalid proxy entry, skip it
78
+ continue
79
+ return False
80
+ except ValueError:
81
+ # Invalid IP address
82
+ return False
83
+
84
+ @classmethod
85
+ def from_global_config(cls) -> "RateLimitConfig":
86
+ """Create RateLimitConfig from GlobalConfig.
87
+
88
+ Uses core/config.py as the single source of truth for
89
+ environment variable handling.
90
+
91
+ Returns:
92
+ RateLimitConfig instance with values from GlobalConfig
93
+ """
94
+ # Import here to avoid circular imports
95
+ from codeframe.core.config import get_global_config
96
+
97
+ global_config = get_global_config()
98
+
99
+ enabled = global_config.rate_limit_enabled
100
+ storage = global_config.rate_limit_storage
101
+ redis_url = global_config.redis_url
102
+
103
+ # Parse trusted proxies from comma-separated string
104
+ trusted_proxies_str = global_config.rate_limit_trusted_proxies.strip()
105
+ trusted_proxies = []
106
+ if trusted_proxies_str:
107
+ trusted_proxies = [
108
+ p.strip() for p in trusted_proxies_str.split(",") if p.strip()
109
+ ]
110
+
111
+ # Validate storage type (already validated by Pydantic, but double-check)
112
+ if storage not in ("memory", "redis"):
113
+ logger.warning(
114
+ f"Invalid RATE_LIMIT_STORAGE: {storage}. "
115
+ f"Must be 'memory' or 'redis'. Defaulting to 'memory'."
116
+ )
117
+ storage = "memory"
118
+
119
+ # Warn if redis storage is requested but no URL provided
120
+ if storage == "redis" and not redis_url:
121
+ logger.warning(
122
+ "RATE_LIMIT_STORAGE is 'redis' but REDIS_URL is not set. "
123
+ "Falling back to in-memory storage."
124
+ )
125
+ storage = "memory"
126
+
127
+ return cls(
128
+ auth_limit=global_config.rate_limit_auth,
129
+ standard_limit=global_config.rate_limit_standard,
130
+ ai_limit=global_config.rate_limit_ai,
131
+ websocket_limit=global_config.rate_limit_websocket,
132
+ enabled=enabled,
133
+ storage=storage,
134
+ redis_url=redis_url,
135
+ trusted_proxies=trusted_proxies,
136
+ )
137
+
138
+
139
+ @lru_cache(maxsize=1)
140
+ def get_rate_limit_config() -> RateLimitConfig:
141
+ """Get the global rate limit configuration.
142
+
143
+ Loads from GlobalConfig on first call, cached thereafter.
144
+ Thread-safe via lru_cache.
145
+
146
+ Returns:
147
+ RateLimitConfig instance
148
+ """
149
+ config = RateLimitConfig.from_global_config()
150
+ logger.info(
151
+ f"Rate limit config initialized: "
152
+ f"enabled={config.enabled}, "
153
+ f"storage={config.storage}, "
154
+ f"standard={config.standard_limit}, "
155
+ f"trusted_proxies={len(config.trusted_proxies)} configured"
156
+ )
157
+ return config
158
+
159
+
160
+ def _reset_rate_limit_config() -> None:
161
+ """Reset the global rate limit configuration.
162
+
163
+ Useful for testing to ensure clean state between tests.
164
+ """
165
+ get_rate_limit_config.cache_clear()
@@ -0,0 +1,15 @@
1
+ """Core components for CodeFRAME orchestration."""
2
+
3
+ from codeframe.core.config import Config
4
+ from codeframe.core.models import Task, TaskStatus, AgentMaturity
5
+ from codeframe.core.stall_detector import StallAction, StallDetectedError, StallDetector
6
+
7
+ __all__ = [
8
+ "Config",
9
+ "Task",
10
+ "TaskStatus",
11
+ "AgentMaturity",
12
+ "StallAction",
13
+ "StallDetectedError",
14
+ "StallDetector",
15
+ ]
@@ -0,0 +1,43 @@
1
+ from codeframe.core.adapters.streaming_chat import (
2
+ ChatEvent,
3
+ ChatEventType,
4
+ StreamingChatAdapter,
5
+ STREAMING_SAFE_TOOLS,
6
+ )
7
+ from codeframe.core.adapters.agent_adapter import (
8
+ AdapterTokenUsage,
9
+ AgentAdapter,
10
+ AgentContext,
11
+ AgentEvent,
12
+ AgentResult,
13
+ AgentResultStatus,
14
+ )
15
+ from codeframe.core.adapters.builtin import (
16
+ BuiltinPlanAdapter,
17
+ BuiltinReactAdapter,
18
+ )
19
+ from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
20
+ from codeframe.core.adapters.codex import CodexAdapter
21
+ from codeframe.core.adapters.opencode import OpenCodeAdapter
22
+ from codeframe.core.adapters.subprocess_adapter import SubprocessAdapter
23
+ from codeframe.core.adapters.verification_wrapper import VerificationWrapper
24
+
25
+ __all__ = [
26
+ "ChatEvent",
27
+ "ChatEventType",
28
+ "StreamingChatAdapter",
29
+ "STREAMING_SAFE_TOOLS",
30
+ "AdapterTokenUsage",
31
+ "AgentAdapter",
32
+ "AgentContext",
33
+ "AgentEvent",
34
+ "AgentResult",
35
+ "AgentResultStatus",
36
+ "BuiltinPlanAdapter",
37
+ "BuiltinReactAdapter",
38
+ "ClaudeCodeAdapter",
39
+ "CodexAdapter",
40
+ "OpenCodeAdapter",
41
+ "SubprocessAdapter",
42
+ "VerificationWrapper",
43
+ ]
@@ -0,0 +1,114 @@
1
+ """Agent adapter protocol for delegating task execution to external coding agents.
2
+
3
+ Defines the interface that any coding agent (Claude Code, Codex, Aider, built-in)
4
+ must implement to be used as a CodeFrame execution engine, plus the supporting
5
+ types for context, results, events, and token tracking.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Callable, Literal, Protocol, runtime_checkable
15
+
16
+
17
+ class AgentResultStatus(str, Enum):
18
+ """Terminal status from an agent execution."""
19
+
20
+ COMPLETED = "completed"
21
+ FAILED = "failed"
22
+ BLOCKED = "blocked"
23
+ TIMEOUT = "timeout"
24
+
25
+
26
+ @dataclass
27
+ class AdapterTokenUsage:
28
+ """Lightweight token usage for adapter results."""
29
+
30
+ input_tokens: int
31
+ output_tokens: int
32
+ model: str | None = None
33
+ cost_usd: float | None = None
34
+
35
+ @property
36
+ def total_tokens(self) -> int:
37
+ return self.input_tokens + self.output_tokens
38
+
39
+
40
+ @dataclass
41
+ class AgentContext:
42
+ """Everything CodeFrame provides to an execution engine."""
43
+
44
+ task_id: str
45
+ task_title: str
46
+ task_description: str
47
+ prd_content: str | None = None
48
+ tech_stack: str | None = None
49
+ project_preferences: str | None = None
50
+ relevant_files: list[str] = field(default_factory=list)
51
+ file_contents: dict[str, str] = field(default_factory=dict)
52
+ blocker_history: list[str] = field(default_factory=list)
53
+ dependency_context: str | None = None
54
+ verification_gates: list[str] = field(default_factory=list)
55
+ attempt: int = 0
56
+ previous_errors: list[str] = field(default_factory=list)
57
+
58
+
59
+ @dataclass
60
+ class AgentResult:
61
+ """Result from an agent adapter execution."""
62
+
63
+ status: Literal["completed", "failed", "blocked"]
64
+ output: str = ""
65
+ modified_files: list[str] = field(default_factory=list)
66
+ error: str | None = None
67
+ blocker_question: str | None = None
68
+ token_usage: AdapterTokenUsage | None = None
69
+ duration_ms: int = 0
70
+ cloud_metadata: dict | None = None
71
+
72
+
73
+ @dataclass
74
+ class AgentEvent:
75
+ """Progress event emitted during agent execution."""
76
+
77
+ type: str # "progress", "tool_call", "output", "error"
78
+ data: dict = field(default_factory=dict)
79
+ message: str = ""
80
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
81
+
82
+
83
+ @runtime_checkable
84
+ class AgentAdapter(Protocol):
85
+ """Protocol for agent execution engines.
86
+
87
+ Any coding agent (Claude Code, OpenCode, Codex, etc.) must implement
88
+ this interface to be used as a CodeFRAME execution engine.
89
+ """
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ """Engine name (e.g., 'claude-code', 'opencode')."""
94
+ ...
95
+
96
+ def run(
97
+ self,
98
+ task_id: str,
99
+ prompt: str,
100
+ workspace_path: Path,
101
+ on_event: Callable[[AgentEvent], None] | None = None,
102
+ ) -> AgentResult:
103
+ """Execute a task and return the result.
104
+
105
+ Args:
106
+ task_id: CodeFRAME task identifier
107
+ prompt: Rich context prompt assembled by TaskContextPackager
108
+ workspace_path: Path to the workspace/repo root
109
+ on_event: Optional callback for streaming progress events
110
+
111
+ Returns:
112
+ AgentResult with status, output, and modified files
113
+ """
114
+ ...
@@ -0,0 +1,326 @@
1
+ """Builtin adapter shims wrapping ReactAgent and Agent behind AgentAdapter protocol.
2
+
3
+ These adapters encapsulate all engine-specific behavior (stall retry for React,
4
+ supervisor retry for Plan) so the runtime can treat all engines uniformly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Callable, Optional
12
+
13
+ from codeframe.core.adapters.agent_adapter import AgentEvent, AgentResult
14
+
15
+ if TYPE_CHECKING:
16
+ from codeframe.adapters.llm.base import LLMProvider
17
+ from codeframe.core.conductor import GlobalFixCoordinator
18
+ from codeframe.core.stall_detector import StallAction
19
+ from codeframe.core.streaming import EventPublisher, RunOutputLogger
20
+ from codeframe.core.workspace import Workspace
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _MAX_STALL_RETRIES = 1
25
+
26
+
27
+ class BuiltinReactAdapter:
28
+ """Wraps the existing ReactAgent behind the AgentAdapter interface.
29
+
30
+ Handles stall detection and retry internally so the runtime
31
+ doesn't need engine-specific branching.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ workspace: Workspace,
37
+ llm_provider: LLMProvider,
38
+ *,
39
+ stall_timeout_s: float = 300,
40
+ stall_action: Optional[StallAction] = None,
41
+ event_publisher: Optional[EventPublisher] = None,
42
+ dry_run: bool = False,
43
+ verbose: bool = False,
44
+ debug: bool = False,
45
+ output_logger: Optional[RunOutputLogger] = None,
46
+ fix_coordinator: Optional[GlobalFixCoordinator] = None,
47
+ ) -> None:
48
+ self._workspace = workspace
49
+ self._llm_provider = llm_provider
50
+ self._stall_timeout_s = stall_timeout_s
51
+ self._stall_action = stall_action
52
+ self._event_publisher = event_publisher
53
+ self._dry_run = dry_run
54
+ self._verbose = verbose
55
+ self._debug = debug
56
+ self._output_logger = output_logger
57
+ self._fix_coordinator = fix_coordinator
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ return "react"
62
+
63
+ @classmethod
64
+ def requirements(cls) -> dict[str, str]:
65
+ """Return requirement names and descriptions."""
66
+ return {"ANTHROPIC_API_KEY": "Anthropic API key for LLM calls"}
67
+
68
+ def run(
69
+ self,
70
+ task_id: str,
71
+ prompt: str,
72
+ workspace_path: Path,
73
+ on_event: Callable[[AgentEvent], None] | None = None,
74
+ ) -> AgentResult:
75
+ """Run the ReactAgent with stall retry, map AgentStatus to AgentResult."""
76
+ from codeframe.core.react_agent import ReactAgent
77
+ from codeframe.core.stall_detector import StallDetectedError
78
+
79
+ def _bridge_event(event_type: str, data: dict) -> None:
80
+ if on_event:
81
+ on_event(AgentEvent(type=event_type, data=data))
82
+
83
+ def _build_agent() -> ReactAgent:
84
+ kwargs: dict = {
85
+ "workspace": self._workspace,
86
+ "llm_provider": self._llm_provider,
87
+ "stall_timeout_s": self._stall_timeout_s,
88
+ "event_publisher": self._event_publisher,
89
+ "dry_run": self._dry_run,
90
+ "verbose": self._verbose,
91
+ "on_event": _bridge_event,
92
+ "debug": self._debug,
93
+ "output_logger": self._output_logger,
94
+ "fix_coordinator": self._fix_coordinator,
95
+ }
96
+ if self._stall_action is not None:
97
+ kwargs["stall_action"] = self._stall_action
98
+ return ReactAgent(**kwargs)
99
+
100
+ for stall_attempt in range(1 + _MAX_STALL_RETRIES):
101
+ try:
102
+ agent = _build_agent()
103
+ status = agent.run(task_id)
104
+ return self._map_status(status)
105
+ except StallDetectedError as exc:
106
+ logger.warning(
107
+ "Stall detected (attempt %d): %s",
108
+ stall_attempt + 1, exc,
109
+ )
110
+ if stall_attempt >= _MAX_STALL_RETRIES:
111
+ logger.error("Max stall retries exceeded, failing task")
112
+ return AgentResult(
113
+ status="failed",
114
+ error=f"Stall detected after {stall_attempt + 1} attempts: {exc}",
115
+ )
116
+ logger.info("Retrying after stall (attempt %d)", stall_attempt + 2)
117
+
118
+ return AgentResult(status="failed", error="Unexpected: stall retry loop exhausted")
119
+
120
+ @staticmethod
121
+ def _map_status(status: object) -> AgentResult:
122
+ """Map AgentStatus enum to AgentResult."""
123
+ from codeframe.core.agent import AgentStatus
124
+
125
+ status_map = {
126
+ AgentStatus.COMPLETED: "completed",
127
+ AgentStatus.FAILED: "failed",
128
+ AgentStatus.BLOCKED: "blocked",
129
+ }
130
+ result_status = status_map.get(status, "failed") # type: ignore[arg-type]
131
+ return AgentResult(
132
+ status=result_status,
133
+ output=f"ReactAgent finished with status: {status.value}", # type: ignore[union-attr]
134
+ )
135
+
136
+
137
+ class BuiltinPlanAdapter:
138
+ """Wraps the existing plan-based Agent behind the AgentAdapter interface.
139
+
140
+ Handles supervisor BLOCKED-retry and tactical failure recovery internally.
141
+ """
142
+
143
+ def __init__(
144
+ self,
145
+ workspace: Workspace,
146
+ llm_provider: LLMProvider,
147
+ *,
148
+ dry_run: bool = False,
149
+ verbose: bool = False,
150
+ debug: bool = False,
151
+ output_logger: Optional[RunOutputLogger] = None,
152
+ fix_coordinator: Optional[GlobalFixCoordinator] = None,
153
+ event_publisher: Optional[EventPublisher] = None,
154
+ ) -> None:
155
+ self._workspace = workspace
156
+ self._llm_provider = llm_provider
157
+ self._dry_run = dry_run
158
+ self._verbose = verbose
159
+ self._debug = debug
160
+ self._output_logger = output_logger
161
+ self._fix_coordinator = fix_coordinator
162
+ self._event_publisher = event_publisher
163
+
164
+ @property
165
+ def name(self) -> str:
166
+ return "plan"
167
+
168
+ @classmethod
169
+ def requirements(cls) -> dict[str, str]:
170
+ """Return requirement names and descriptions."""
171
+ return {"ANTHROPIC_API_KEY": "Anthropic API key for LLM calls"}
172
+
173
+ def run(
174
+ self,
175
+ task_id: str,
176
+ prompt: str,
177
+ workspace_path: Path,
178
+ on_event: Callable[[AgentEvent], None] | None = None,
179
+ ) -> AgentResult:
180
+ """Run the plan-based Agent with supervisor retry, map to AgentResult."""
181
+ from codeframe.core.agent import Agent, AgentStatus
182
+
183
+ def _bridge_event(event_type: str, data: dict) -> None:
184
+ if on_event:
185
+ on_event(AgentEvent(type=event_type, data=data))
186
+
187
+ def _build_agent() -> Agent:
188
+ return Agent(
189
+ workspace=self._workspace,
190
+ llm_provider=self._llm_provider,
191
+ dry_run=self._dry_run,
192
+ on_event=_bridge_event,
193
+ debug=self._debug,
194
+ verbose=self._verbose,
195
+ fix_coordinator=self._fix_coordinator,
196
+ output_logger=self._output_logger,
197
+ event_publisher=self._event_publisher,
198
+ )
199
+
200
+ agent = _build_agent()
201
+ state = agent.run(task_id)
202
+
203
+ # Supervisor BLOCKED-retry (best-effort — failures don't affect result)
204
+ if state.status == AgentStatus.BLOCKED:
205
+ try:
206
+ state = self._try_supervisor_unblock(state, task_id, _build_agent)
207
+ except Exception:
208
+ logger.debug("Supervisor unblock failed, returning original state", exc_info=True)
209
+
210
+ # Supervisor tactical failure recovery (best-effort)
211
+ if state.status == AgentStatus.FAILED:
212
+ try:
213
+ state = self._try_tactical_recovery(state, task_id, _build_agent)
214
+ except Exception:
215
+ logger.debug("Supervisor tactical recovery failed, returning original state", exc_info=True)
216
+
217
+ return self._map_state(state)
218
+
219
+ def _try_supervisor_unblock(self, state, task_id: str, build_agent):
220
+ """Try supervisor resolution for BLOCKED tasks."""
221
+ from codeframe.core.conductor import get_supervisor
222
+
223
+ supervisor = get_supervisor(self._workspace)
224
+ if supervisor.try_resolve_blocked_task(task_id):
225
+ logger.info("[Supervisor] Retrying task after auto-resolution...")
226
+ agent = build_agent()
227
+ state = agent.run(task_id)
228
+ return state
229
+
230
+ def _try_tactical_recovery(self, state, task_id: str, build_agent):
231
+ """Try supervisor tactical recovery for FAILED tasks."""
232
+ from codeframe.core.conductor import get_supervisor, SUPERVISOR_TACTICAL_PATTERNS
233
+
234
+ error_msg = self._extract_error(state)
235
+
236
+ if not error_msg:
237
+ return state
238
+
239
+ error_msg_lower = error_msg.lower()
240
+ matched_patterns = [p for p in SUPERVISOR_TACTICAL_PATTERNS if p in error_msg_lower]
241
+
242
+ if not matched_patterns:
243
+ return state
244
+
245
+ supervisor = get_supervisor(self._workspace)
246
+ resolution = supervisor._generate_tactical_resolution(error_msg)
247
+ logger.info(
248
+ "Supervisor detected recoverable error, providing guidance: %s...",
249
+ resolution[:100],
250
+ )
251
+
252
+ from codeframe.core import blockers
253
+ blocker = blockers.create(
254
+ self._workspace,
255
+ task_id=task_id,
256
+ question=f"Technical error: {error_msg[:500]}",
257
+ created_by="agent",
258
+ )
259
+ blockers.answer(self._workspace, blocker.id, resolution)
260
+
261
+ logger.info("Supervisor retrying task with guidance...")
262
+ agent = build_agent()
263
+ return agent.run(task_id)
264
+
265
+ @staticmethod
266
+ def _extract_error(state) -> str:
267
+ """Extract error message from AgentState for supervisor analysis."""
268
+ blocker = getattr(state, "blocker", None)
269
+ if blocker:
270
+ return getattr(blocker, "reason", None) or getattr(blocker, "question", None) or ""
271
+
272
+ step_results = getattr(state, "step_results", None) or []
273
+ if step_results:
274
+ last_result = step_results[-1]
275
+ if hasattr(last_result, "error") and last_result.error:
276
+ return last_result.error
277
+ if hasattr(last_result, "output") and last_result.output:
278
+ return last_result.output
279
+
280
+ gate_results = getattr(state, "gate_results", None) or []
281
+ for gate in gate_results:
282
+ if not gate.passed:
283
+ for check in gate.checks:
284
+ if check.output:
285
+ return check.output
286
+
287
+ return ""
288
+
289
+ @staticmethod
290
+ def _map_state(state: object) -> AgentResult:
291
+ """Map AgentState dataclass to AgentResult."""
292
+ from codeframe.core.agent import AgentStatus
293
+
294
+ status_map = {
295
+ AgentStatus.COMPLETED: "completed",
296
+ AgentStatus.FAILED: "failed",
297
+ AgentStatus.BLOCKED: "blocked",
298
+ }
299
+ result_status = status_map.get(state.status, "failed") # type: ignore[union-attr]
300
+
301
+ blocker_question = None
302
+ if state.status == AgentStatus.BLOCKED: # type: ignore[union-attr]
303
+ blocker = getattr(state, "blocker", None)
304
+ if blocker:
305
+ blocker_question = getattr(blocker, "question", None) or getattr(
306
+ blocker, "reason", None
307
+ )
308
+
309
+ error = None
310
+ if state.status == AgentStatus.FAILED: # type: ignore[union-attr]
311
+ gate_results = getattr(state, "gate_results", None) or []
312
+ error_parts: list[str] = []
313
+ for gr in gate_results:
314
+ for check in getattr(gr, "checks", []):
315
+ output = getattr(check, "output", None)
316
+ if output:
317
+ error_parts.append(output)
318
+ if error_parts:
319
+ error = "\n".join(error_parts[:3])
320
+
321
+ return AgentResult(
322
+ status=result_status,
323
+ output=f"PlanAgent finished with status: {state.status.value}", # type: ignore[union-attr]
324
+ blocker_question=blocker_question,
325
+ error=error,
326
+ )