File size: 8,106 Bytes
92c4ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | """
Auto-Dev Capability Gate
Gates Auto-Dev features based on agent maturity level (graduation framework)
and workspace settings. This ensures:
1. Only agents that have proven competence can use self-evolution features
2. Users must explicitly enable Auto-Dev in workspace settings
3. Users are notified when agents graduate into new capabilities
Maturity requirements:
- auto_dev.memento_skills → INTERN (skill suggestions, user approval required)
- auto_dev.alpha_evolver → SUPERVISED (mutation loops, results queued for review)
- auto_dev.background_evolution → AUTONOMOUS (background loops, sandbox-gated)
"""
import logging
from typing import Any
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Maturity level constants (mirrors CapabilityMaturityLevel from graduation service)
STUDENT = "student"
INTERN = "intern"
SUPERVISED = "supervised"
AUTONOMOUS = "autonomous"
MATURITY_ORDER = [STUDENT, INTERN, SUPERVISED, AUTONOMOUS]
def is_at_least(current: str, required: str) -> bool:
"""Check if current maturity level meets or exceeds the required level."""
if current not in MATURITY_ORDER or required not in MATURITY_ORDER:
return False
return MATURITY_ORDER.index(current) >= MATURITY_ORDER.index(required)
class AutoDevCapabilityService:
"""Gates Auto-Dev features based on agent maturity and workspace settings."""
# Maturity requirements for each Auto-Dev capability
CAPABILITY_GATES: dict[str, str] = {
"auto_dev.memento_skills": INTERN,
"auto_dev.alpha_evolver": SUPERVISED,
"auto_dev.background_evolution": AUTONOMOUS,
}
def __init__(self, db: Session):
self.db = db
self._graduation_service = None
@property
def graduation(self):
"""Lazy-load graduation service to avoid circular imports."""
if self._graduation_service is None:
try:
from core.capability_graduation_service import (
CapabilityGraduationService,
)
self._graduation_service = CapabilityGraduationService(self.db)
except ImportError:
logger.warning(
"CapabilityGraduationService not available — "
"Auto-Dev capabilities will default to STUDENT (disabled)"
)
return self._graduation_service
def can_use(
self,
agent_id: str,
capability: str,
workspace_settings: dict[str, Any] | None = None,
) -> bool:
"""
Check if an agent can use a specific Auto-Dev capability.
Requires BOTH:
1. Workspace settings allow it (auto_dev.enabled + per-capability toggle)
2. Agent has graduated to the required maturity level
Args:
agent_id: The agent to check
capability: e.g., "auto_dev.memento_skills"
workspace_settings: Workspace.configuration dict (or Tenant.configuration)
Returns:
True if the agent is allowed to use this capability
"""
settings = workspace_settings or {}
# 1. Check workspace-level toggle
auto_dev_settings = settings.get("auto_dev", {})
if not auto_dev_settings.get("enabled", False):
return False
# 2. Check capability-specific toggle
cap_key = capability.split(".")[-1] # e.g., "memento_skills"
if not auto_dev_settings.get(cap_key, True):
return False
# 3. Check graduation maturity gate
required = self.CAPABILITY_GATES.get(capability)
if not required:
logger.warning(f"Unknown Auto-Dev capability: {capability}")
return False
current = self._get_agent_maturity(agent_id, capability)
return is_at_least(current, required)
def record_usage(self, agent_id: str, capability: str, success: bool) -> None:
"""
Record Auto-Dev usage to progress agent maturity via graduation framework.
Each successful use contributes to the agent's graduation score for
this capability, eventually unlocking higher tiers.
"""
if self.graduation:
try:
self.graduation.record_usage(agent_id, capability, success)
except Exception as e:
logger.error(f"Failed to record Auto-Dev usage: {e}")
def check_daily_limits(
self,
agent_id: str,
capability: str,
workspace_settings: dict[str, Any] | None = None,
) -> bool:
"""
Check if the agent has exceeded daily Auto-Dev limits.
Limits are defined in workspace settings:
- max_mutations_per_day (default: 10)
- max_skill_candidates_per_day (default: 5)
"""
settings = (workspace_settings or {}).get("auto_dev", {})
if capability == "auto_dev.alpha_evolver":
max_daily = settings.get("max_mutations_per_day", 10)
elif capability == "auto_dev.memento_skills":
max_daily = settings.get("max_skill_candidates_per_day", 5)
else:
return True
# Count today's usage from models
from datetime import datetime, timezone
today_start = datetime.now(timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
try:
if capability == "auto_dev.alpha_evolver":
from core.auto_dev.models import ToolMutation
count = (
self.db.query(ToolMutation)
.filter(
ToolMutation.tenant_id == self._get_agent_tenant(agent_id),
ToolMutation.created_at >= today_start,
)
.count()
)
elif capability == "auto_dev.memento_skills":
from core.auto_dev.models import SkillCandidate
count = (
self.db.query(SkillCandidate)
.filter(
SkillCandidate.agent_id == agent_id,
SkillCandidate.created_at >= today_start,
)
.count()
)
else:
return True
return count < max_daily
except Exception as e:
logger.error(f"Failed to check daily limits: {e}")
return True # Fail open to avoid blocking the system
def notify_capability_unlocked(
self, agent_id: str, capability: str
) -> dict[str, Any]:
"""
Generate notification payload when an agent graduates into a new capability.
The caller (e.g., GraduationService) should deliver this notification
to the user via the appropriate channel.
"""
cap_display = capability.replace("auto_dev.", "").replace("_", " ").title()
return {
"type": "auto_dev_capability_unlocked",
"agent_id": agent_id,
"capability": capability,
"message": (
f"Agent has graduated to use {cap_display}. "
f"Enable it in Settings > Auto-Dev to activate."
),
"action_required": True,
}
def _get_agent_maturity(self, agent_id: str, capability: str) -> str:
"""Get agent's current maturity for a capability."""
if self.graduation:
try:
return self.graduation.get_maturity(agent_id, capability)
except Exception as e:
logger.error(f"Failed to get maturity for {agent_id}: {e}")
return STUDENT
def _get_agent_tenant(self, agent_id: str) -> str | None:
"""Get tenant_id for an agent."""
try:
from core.models import AgentRegistry
agent = (
self.db.query(AgentRegistry)
.filter(AgentRegistry.id == agent_id)
.first()
)
return agent.tenant_id if agent else None
except Exception:
return None
|