aureon-os / aureon /code_architect /skill_library.py
GaryLeckey's picture
Deploy Aureon OS (deploy/selfhost stack: nginx edge + fusion API x2 + console + HNC organ + redis + postgres; paper mode) (part 3)
1ccf652 verified
Raw History Blame Contribute Delete
14.2 kB
"""
SkillLibrary β€” the persistent registry of learned skills
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Persists skills to JSON under state/skills/. Supports:
- by-name lookup (fast path)
- by-level filter
- by-category filter
- by-tag filter
- dependency resolution (given a skill, return the full tree of deps)
- execution-stat updates (success/failure/duration)
- atomic bulk save/load
"""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from aureon.code_architect.skill import Skill, SkillLevel, SkillStatus
logger = logging.getLogger("aureon.code_architect.library")
class SkillLibrary:
"""Thread-safe persistent skill registry."""
DEFAULT_STORAGE_DIR = Path("state/skills")
LIBRARY_FILE = "skill_library.json"
def __init__(self, storage_dir: Optional[Path] = None):
self.storage_dir = Path(storage_dir or self.DEFAULT_STORAGE_DIR)
self.storage_dir.mkdir(parents=True, exist_ok=True)
self.library_path = self.storage_dir / self.LIBRARY_FILE
self._skills: Dict[str, Skill] = {} # name β†’ Skill
self._lock = threading.RLock()
self._load()
# ─────────────────────────────────────────────────────────────────────
# Load / save
# ─────────────────────────────────────────────────────────────────────
def _load(self) -> None:
if not self.library_path.exists():
return
try:
with open(self.library_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
skills = data.get("skills", [])
elif isinstance(data, list):
skills = data
else:
skills = []
with self._lock:
for skill_data in skills:
try:
skill = Skill.from_dict(skill_data)
self._skills[skill.name] = skill
except Exception as e:
logger.warning("Failed to load skill %s: %s", skill_data.get("name"), e)
logger.info("Loaded %d skills from %s", len(self._skills), self.library_path)
except Exception as e:
logger.warning("Library load failed: %s", e)
def save(self) -> None:
with self._lock:
data = {
"version": 1,
"saved_at": time.time(),
"count": len(self._skills),
"skills": [s.to_dict() for s in self._skills.values()],
}
try:
tmp = self.library_path.with_suffix(".json.tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
os.replace(tmp, self.library_path)
except Exception as e:
logger.warning("Library save failed: %s", e)
# ─────────────────────────────────────────────────────────────────────
# CRUD
# ─────────────────────────────────────────────────────────────────────
def add(self, skill: Skill, persist: bool = True) -> Skill:
"""Add or overwrite a skill."""
if not skill.name:
raise ValueError("skill.name is required")
with self._lock:
self._skills[skill.name] = skill
if persist:
self.save()
return skill
def get(self, name: str) -> Optional[Skill]:
with self._lock:
return self._skills.get(name)
def remove(self, name: str, persist: bool = True) -> bool:
with self._lock:
removed = self._skills.pop(name, None) is not None
if removed and persist:
self.save()
return removed
def contains(self, name: str) -> bool:
with self._lock:
return name in self._skills
def __contains__(self, name: str) -> bool:
return self.contains(name)
def __len__(self) -> int:
with self._lock:
return len(self._skills)
# ─────────────────────────────────────────────────────────────────────
# Filters
# ─────────────────────────────────────────────────────────────────────
def all(self) -> List[Skill]:
with self._lock:
return list(self._skills.values())
def by_level(self, level: SkillLevel) -> List[Skill]:
with self._lock:
return [s for s in self._skills.values() if s.level == level]
def by_category(self, category: str) -> List[Skill]:
with self._lock:
return [s for s in self._skills.values() if s.category == category]
def by_tag(self, tag: str) -> List[Skill]:
with self._lock:
return [s for s in self._skills.values() if tag in s.tags]
def by_status(self, status: SkillStatus) -> List[Skill]:
with self._lock:
return [s for s in self._skills.values() if s.status == status]
def search(self, substring: str) -> List[Skill]:
substring = substring.lower()
with self._lock:
return [
s for s in self._skills.values()
if substring in s.name.lower() or substring in s.description.lower()
]
# ─────────────────────────────────────────────────────────────────────
# Dependency resolution
# ─────────────────────────────────────────────────────────────────────
def resolve_dependencies(self, name: str) -> List[Skill]:
"""
Return the full transitive dependency closure of a skill
topologically sorted (dependencies before the skill itself).
Iterative implementation so very deep chains (thousands of levels)
don't hit Python's recursion limit. Handles missing deps and
cycles gracefully (missing deps are skipped, cycles are broken).
"""
root = self.get(name)
if not root:
return []
# Iterative DFS with post-order emit for topological sort.
# Stack entries: (skill, child_index)
ordered: List[Skill] = []
seen_in_order: set = set() # names already appended to `ordered`
on_stack: set = set() # names currently on the DFS stack (cycle guard)
stack: List[Any] = [[root, 0]]
on_stack.add(root.name)
while stack:
top = stack[-1]
skill, idx = top[0], top[1]
if idx >= len(skill.dependencies):
# All children processed β€” emit this skill
stack.pop()
on_stack.discard(skill.name)
if skill.name not in seen_in_order:
ordered.append(skill)
seen_in_order.add(skill.name)
continue
# Advance the iterator before descending
top[1] = idx + 1
dep_name = skill.dependencies[idx]
if dep_name in seen_in_order:
continue # already emitted
if dep_name in on_stack:
continue # cycle β€” skip the back edge
dep = self.get(dep_name)
if dep is None:
continue # missing dep β€” skip
stack.append([dep, 0])
on_stack.add(dep_name)
return ordered
def has_cycles(self) -> bool:
"""Check if the skill graph contains cycles (iterative DFS)."""
with self._lock:
visited: set = set()
for start_name in list(self._skills.keys()):
if start_name in visited:
continue
if self._detect_cycle_iterative(start_name, visited):
return True
return False
def _detect_cycle_iterative(self, start: str, global_visited: set) -> bool:
"""Iterative DFS cycle detector for one connected component."""
on_stack: set = set()
stack: List[Any] = [[start, 0]]
on_stack.add(start)
while stack:
top = stack[-1]
name, idx = top[0], top[1]
skill = self._skills.get(name)
if skill is None or idx >= len(skill.dependencies):
stack.pop()
on_stack.discard(name)
global_visited.add(name)
continue
top[1] = idx + 1
dep_name = skill.dependencies[idx]
if dep_name in on_stack:
return True # back edge β†’ cycle
if dep_name in global_visited:
continue
if dep_name in self._skills:
stack.append([dep_name, 0])
on_stack.add(dep_name)
return False
# ─────────────────────────────────────────────────────────────────────
# Execution stat updates
# ─────────────────────────────────────────────────────────────────────
def record_execution(
self,
name: str,
success: bool,
duration_s: float,
error: Optional[str] = None,
persist: bool = False,
) -> None:
with self._lock:
skill = self._skills.get(name)
if not skill:
return
skill.execution_count += 1
if success:
skill.success_count += 1
else:
skill.failure_count += 1
skill.last_error = error
skill.last_execution_at = time.time()
# Online mean
if skill.execution_count == 1:
skill.mean_duration_s = duration_s
else:
n = skill.execution_count
skill.mean_duration_s = skill.mean_duration_s + (duration_s - skill.mean_duration_s) / n
if persist:
self.save()
# ─────────────────────────────────────────────────────────────────────
# Stats
# ─────────────────────────────────────────────────────────────────────
def get_stats(self) -> Dict[str, Any]:
with self._lock:
by_level: Dict[str, int] = {}
by_status: Dict[str, int] = {}
by_category: Dict[str, int] = {}
total_exec = 0
total_success = 0
for s in self._skills.values():
by_level[s.level.name] = by_level.get(s.level.name, 0) + 1
by_status[s.status.value] = by_status.get(s.status.value, 0) + 1
by_category[s.category] = by_category.get(s.category, 0) + 1
total_exec += s.execution_count
total_success += s.success_count
return {
"total_skills": len(self._skills),
"by_level": by_level,
"by_status": by_status,
"by_category": by_category,
"total_executions": total_exec,
"total_successes": total_success,
"overall_success_rate": (
total_success / total_exec if total_exec > 0 else 0.0
),
"has_cycles": self.has_cycles(),
"storage_path": str(self.library_path),
}
# ─────────────────────────────────────────────────────────────────────────────
# Singleton
# ─────────────────────────────────────────────────────────────────────────────
_library_instance: Optional[SkillLibrary] = None
_library_lock = threading.Lock()
def get_skill_library(storage_dir: Optional[Path] = None) -> SkillLibrary:
"""Get or create the singleton SkillLibrary."""
global _library_instance
with _library_lock:
if _library_instance is None:
_library_instance = SkillLibrary(storage_dir=storage_dir)
return _library_instance