Add kaizen-agentic feedback CLI, Gitea issue templates, CI workflow, pre-commit hooks, FEEDBACK/TELEMETRY docs, and cross-platform path tests. Improve CLI registry error messages; remove agents_backup scaffolding. Apply black formatting across src/tests for CI consistency. State Hub message sent to agentic-resources for Helix correlation doc link.
306 lines
10 KiB
Python
306 lines
10 KiB
Python
"""Agent registry and management functionality."""
|
|
|
|
import re
|
|
import yaml
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
|
|
class AgentCategory(Enum):
|
|
"""Categories of agents for organization."""
|
|
|
|
PROJECT_MANAGEMENT = "project-management"
|
|
DEVELOPMENT_PROCESS = "development-process"
|
|
CODE_QUALITY = "code-quality"
|
|
INFRASTRUCTURE = "infrastructure"
|
|
TESTING = "testing"
|
|
DOCUMENTATION = "documentation"
|
|
META = "meta"
|
|
|
|
|
|
@dataclass
|
|
class AgentDefinition:
|
|
"""Represents an agent definition with metadata."""
|
|
|
|
name: str
|
|
description: str
|
|
file_path: Path
|
|
category: AgentCategory
|
|
dependencies: Set[str]
|
|
model: Optional[str] = None
|
|
memory: Optional[str] = None # "enabled" (default) | "disabled"
|
|
|
|
@classmethod
|
|
def from_file(cls, file_path: Path) -> "AgentDefinition":
|
|
"""Create AgentDefinition from a markdown file."""
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
# Extract YAML frontmatter
|
|
frontmatter_match = re.match(r"^---\n(.*?)\n---\n", content, re.DOTALL)
|
|
if not frontmatter_match:
|
|
raise ValueError(f"No YAML frontmatter found in {file_path}")
|
|
|
|
frontmatter = yaml.safe_load(frontmatter_match.group(1))
|
|
|
|
# Extract dependencies from frontmatter and content
|
|
dependencies = cls._extract_dependencies(content, frontmatter)
|
|
|
|
# Determine category from name or content
|
|
category = cls._determine_category(frontmatter["name"], content)
|
|
|
|
return cls(
|
|
name=frontmatter["name"],
|
|
description=frontmatter["description"],
|
|
file_path=file_path,
|
|
category=category,
|
|
dependencies=dependencies,
|
|
model=frontmatter.get("model"),
|
|
memory=frontmatter.get("memory"),
|
|
)
|
|
|
|
@staticmethod
|
|
def _extract_dependencies(content: str, frontmatter: dict) -> Set[str]:
|
|
"""Extract agent dependencies from frontmatter and content."""
|
|
dependencies = set()
|
|
|
|
# Check frontmatter for explicit dependencies
|
|
for key in ["dependencies", "depends_on", "requires"]:
|
|
if key in frontmatter:
|
|
deps = frontmatter[key]
|
|
if isinstance(deps, list):
|
|
dependencies.update(deps)
|
|
elif isinstance(deps, str):
|
|
# Handle comma-separated string
|
|
dependencies.update([d.strip() for d in deps.split(",")])
|
|
|
|
# Look for explicit dependencies in content
|
|
dep_patterns = [
|
|
r"depends_on:\s*\[(.*?)\]",
|
|
r"requires:\s*\[(.*?)\]",
|
|
r"dependencies:\s*\[(.*?)\]",
|
|
]
|
|
|
|
for pattern in dep_patterns:
|
|
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
for match in matches:
|
|
if isinstance(match, str):
|
|
deps = [d.strip().strip("\"'") for d in match.split(",")]
|
|
dependencies.update(deps)
|
|
|
|
# Look for specific agent references in content (more precise)
|
|
# Only look for full agent names like "todo-keeper agent" or "uses changelog-keeper"
|
|
agent_patterns = [
|
|
r"uses?\s+(\w+(?:-\w+)*-(?:keeper|agent|workflow|helper|manager))",
|
|
r"depends?\s+on\s+(\w+(?:-\w+)*-(?:keeper|agent|workflow|helper|manager))",
|
|
r"requires?\s+(\w+(?:-\w+)*-(?:keeper|agent|workflow|helper|manager))",
|
|
]
|
|
|
|
for pattern in agent_patterns:
|
|
matches = re.findall(pattern, content.lower())
|
|
for match in matches:
|
|
if match not in ["optimization", "agentic", "driven", "assisted"]:
|
|
dependencies.add(match.replace("-", "_"))
|
|
|
|
return dependencies
|
|
|
|
@staticmethod
|
|
def _determine_category(name: str, content: str) -> AgentCategory:
|
|
"""Determine agent category based on name and content."""
|
|
name_lower = name.lower()
|
|
|
|
# Project management agents
|
|
project_keywords = ["todo", "changelog", "contributing", "project"]
|
|
if any(keyword in name_lower for keyword in project_keywords):
|
|
return AgentCategory.PROJECT_MANAGEMENT
|
|
|
|
# Testing agents
|
|
if any(keyword in name_lower for keyword in ["test", "tdd"]):
|
|
return AgentCategory.TESTING
|
|
|
|
# Code quality agents
|
|
if any(
|
|
keyword in name_lower for keyword in ["refactor", "optimization", "code"]
|
|
):
|
|
return AgentCategory.CODE_QUALITY
|
|
|
|
# Documentation agents
|
|
if any(keyword in name_lower for keyword in ["documentation", "claude"]):
|
|
return AgentCategory.DOCUMENTATION
|
|
|
|
# Meta agents (coaching, cross-agent orchestration)
|
|
if any(keyword in name_lower for keyword in ["coach", "meta"]):
|
|
return AgentCategory.META
|
|
|
|
# Infrastructure agents
|
|
if any(
|
|
keyword in name_lower
|
|
for keyword in ["setup", "repository", "tooling", "sys-medic", "medic"]
|
|
):
|
|
return AgentCategory.INFRASTRUCTURE
|
|
|
|
# Development process agents
|
|
if any(
|
|
keyword in name_lower
|
|
for keyword in ["workflow", "requirements", "maintenance"]
|
|
):
|
|
return AgentCategory.DEVELOPMENT_PROCESS
|
|
|
|
# Default fallback
|
|
return AgentCategory.DEVELOPMENT_PROCESS
|
|
|
|
|
|
class AgentRegistry:
|
|
"""Registry for managing and discovering agents."""
|
|
|
|
def __init__(self, agents_dir: Path):
|
|
self.agents_dir = Path(agents_dir)
|
|
self._agents: Dict[str, AgentDefinition] = {}
|
|
self._load_agents()
|
|
|
|
def _load_agents(self):
|
|
"""Load all agents from the agents directory."""
|
|
if not self.agents_dir.exists():
|
|
return
|
|
|
|
for agent_file in self.agents_dir.glob("agent-*.md"):
|
|
try:
|
|
agent_def = AgentDefinition.from_file(agent_file)
|
|
self._agents[agent_def.name] = agent_def
|
|
except Exception as e:
|
|
print(f"Warning: Failed to load agent {agent_file}: {e}")
|
|
|
|
def get_agent(self, name: str) -> Optional[AgentDefinition]:
|
|
"""Get agent definition by name."""
|
|
return self._agents.get(name)
|
|
|
|
def list_agents(
|
|
self, category: Optional[AgentCategory] = None
|
|
) -> List[AgentDefinition]:
|
|
"""List all agents, optionally filtered by category."""
|
|
agents = list(self._agents.values())
|
|
if category:
|
|
agents = [a for a in agents if a.category == category]
|
|
return sorted(agents, key=lambda a: a.name)
|
|
|
|
def get_categories(self) -> Dict[AgentCategory, List[AgentDefinition]]:
|
|
"""Get agents organized by category."""
|
|
categories = {}
|
|
for agent in self._agents.values():
|
|
if agent.category not in categories:
|
|
categories[agent.category] = []
|
|
categories[agent.category].append(agent)
|
|
|
|
# Sort agents within each category
|
|
for category in categories:
|
|
categories[category].sort(key=lambda a: a.name)
|
|
|
|
return categories
|
|
|
|
def resolve_dependencies(self, agent_names: List[str]) -> List[str]:
|
|
"""Resolve agent dependencies and return ordered list."""
|
|
resolved = []
|
|
visited = set()
|
|
|
|
def visit(name: str):
|
|
if name in visited:
|
|
return
|
|
visited.add(name)
|
|
|
|
agent = self.get_agent(name)
|
|
if not agent:
|
|
print(f"Warning: Agent '{name}' not found")
|
|
return
|
|
|
|
# Visit dependencies first
|
|
for dep in agent.dependencies:
|
|
visit(dep)
|
|
|
|
if name not in resolved:
|
|
resolved.append(name)
|
|
|
|
for name in agent_names:
|
|
visit(name)
|
|
|
|
return resolved
|
|
|
|
def validate_agents(self) -> Dict[str, List[str]]:
|
|
"""Validate all agents and return validation errors."""
|
|
errors = {}
|
|
|
|
for name, agent in self._agents.items():
|
|
agent_errors = []
|
|
|
|
# Check for missing dependencies
|
|
for dep in agent.dependencies:
|
|
if dep not in self._agents:
|
|
agent_errors.append(f"Missing dependency: {dep}")
|
|
|
|
# Check file exists
|
|
if not agent.file_path.exists():
|
|
agent_errors.append(f"Agent file not found: {agent.file_path}")
|
|
|
|
# Check for circular dependencies
|
|
if self._has_circular_dependency(name):
|
|
agent_errors.append("Circular dependency detected")
|
|
|
|
if agent_errors:
|
|
errors[name] = agent_errors
|
|
|
|
return errors
|
|
|
|
def _has_circular_dependency(
|
|
self, agent_name: str, visited: Optional[Set[str]] = None
|
|
) -> bool:
|
|
"""Check if an agent has circular dependencies."""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
if agent_name in visited:
|
|
return True
|
|
|
|
agent = self.get_agent(agent_name)
|
|
if not agent:
|
|
return False
|
|
|
|
visited.add(agent_name)
|
|
|
|
for dep in agent.dependencies:
|
|
if self._has_circular_dependency(dep, visited.copy()):
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_agent_templates(self) -> Dict[str, List[str]]:
|
|
"""Get predefined agent templates for different project types."""
|
|
return {
|
|
"python-basic": ["setupRepository", "keepaTodofile", "keepaChangelog"],
|
|
"python-web": [
|
|
"setupRepository",
|
|
"tdd-workflow",
|
|
"code-refactoring",
|
|
"keepaTodofile",
|
|
"keepaChangelog",
|
|
"keepaContributingfile",
|
|
],
|
|
"python-cli": [
|
|
"setupRepository",
|
|
"tdd-workflow",
|
|
"testing-efficiency",
|
|
"claude-documentation",
|
|
"keepaTodofile",
|
|
"keepaChangelog",
|
|
],
|
|
"python-data": [
|
|
"setupRepository",
|
|
"datamodel-optimization",
|
|
"testing-efficiency",
|
|
"requirements-engineering",
|
|
"keepaTodofile",
|
|
"keepaChangelog",
|
|
],
|
|
"comprehensive": [agent.name for agent in self.list_agents()],
|
|
}
|