| """ |
| 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__) |
|
|
|
|
| |
| 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.""" |
|
|
| |
| 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 {} |
|
|
| |
| auto_dev_settings = settings.get("auto_dev", {}) |
| if not auto_dev_settings.get("enabled", False): |
| return False |
|
|
| |
| cap_key = capability.split(".")[-1] |
| if not auto_dev_settings.get(cap_key, True): |
| return False |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| 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 |
|
|