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,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
|
+
)
|