mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 22:32:06 +00:00
Major feature implementation for parallel agent execution with dependency-aware scheduling and an engaging multi-agent UI experience. Backend Changes: - Add parallel_orchestrator.py for concurrent feature processing - Add api/dependency_resolver.py with cycle detection (Kahn's algorithm + DFS) - Add atomic feature_claim_next() with retry limit and exponential backoff - Fix circular dependency check arguments in 4 locations - Add AgentTracker class for parsing agent output and emitting updates - Add browser isolation with --isolated flag for Playwright MCP - Extend WebSocket protocol with agent_update messages and log attribution - Add WSAgentUpdateMessage schema with agent states and mascot names - Fix WSProgressMessage to include in_progress field New UI Components: - AgentMissionControl: Dashboard showing active agents with collapsible activity - AgentCard: Individual agent status with avatar and thought bubble - AgentAvatar: SVG mascots (Spark, Fizz, Octo, Hoot, Buzz) with animations - ActivityFeed: Recent activity stream with stable keys (no flickering) - CelebrationOverlay: Confetti animation with click/Escape dismiss - DependencyGraph: Interactive node graph visualization with dagre layout - DependencyBadge: Visual indicator for feature dependencies - ViewToggle: Switch between Kanban and Graph views - KeyboardShortcutsHelp: Help overlay accessible via ? key UI/UX Improvements: - Celebration queue system to handle rapid success messages - Accessibility attributes on AgentAvatar (role, aria-label, aria-live) - Collapsible Recent Activity section with persisted preference - Agent count display in header - Keyboard shortcut G to toggle Kanban/Graph view - Real-time thought bubbles and state animations Bug Fixes: - Fix circular dependency validation (swapped source/target arguments) - Add MAX_CLAIM_RETRIES=10 to prevent stack overflow under contention - Fix THOUGHT_PATTERNS to match actual [Tool: name] format - Fix ActivityFeed key prop to prevent re-renders on new items - Add featureId/agentIndex to log messages for proper attribution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
162 lines
4.6 KiB
Python
162 lines
4.6 KiB
Python
"""
|
|
Agent Router
|
|
============
|
|
|
|
API endpoints for agent control (start/stop/pause/resume).
|
|
Uses project registry for path lookups.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from ..schemas import AgentActionResponse, AgentStartRequest, AgentStatus
|
|
from ..services.process_manager import get_manager
|
|
|
|
|
|
def _get_project_path(project_name: str) -> Path:
|
|
"""Get project path from registry."""
|
|
import sys
|
|
root = Path(__file__).parent.parent.parent
|
|
if str(root) not in sys.path:
|
|
sys.path.insert(0, str(root))
|
|
|
|
from registry import get_project_path
|
|
return get_project_path(project_name)
|
|
|
|
|
|
def _get_settings_defaults() -> tuple[bool, str]:
|
|
"""Get YOLO mode and model defaults from global settings."""
|
|
import sys
|
|
root = Path(__file__).parent.parent.parent
|
|
if str(root) not in sys.path:
|
|
sys.path.insert(0, str(root))
|
|
|
|
from registry import DEFAULT_MODEL, get_all_settings
|
|
|
|
settings = get_all_settings()
|
|
yolo_mode = (settings.get("yolo_mode") or "false").lower() == "true"
|
|
model = settings.get("model", DEFAULT_MODEL)
|
|
return yolo_mode, model
|
|
|
|
|
|
router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
|
|
|
|
# Root directory for process manager
|
|
ROOT_DIR = Path(__file__).parent.parent.parent
|
|
|
|
|
|
def validate_project_name(name: str) -> str:
|
|
"""Validate and sanitize project name to prevent path traversal."""
|
|
if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Invalid project name"
|
|
)
|
|
return name
|
|
|
|
|
|
def get_project_manager(project_name: str):
|
|
"""Get the process manager for a project."""
|
|
project_name = validate_project_name(project_name)
|
|
project_dir = _get_project_path(project_name)
|
|
|
|
if not project_dir:
|
|
raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found in registry")
|
|
|
|
if not project_dir.exists():
|
|
raise HTTPException(status_code=404, detail=f"Project directory not found: {project_dir}")
|
|
|
|
return get_manager(project_name, project_dir, ROOT_DIR)
|
|
|
|
|
|
@router.get("/status", response_model=AgentStatus)
|
|
async def get_agent_status(project_name: str):
|
|
"""Get the current status of the agent for a project."""
|
|
manager = get_project_manager(project_name)
|
|
|
|
# Run healthcheck to detect crashed processes
|
|
await manager.healthcheck()
|
|
|
|
return AgentStatus(
|
|
status=manager.status,
|
|
pid=manager.pid,
|
|
started_at=manager.started_at,
|
|
yolo_mode=manager.yolo_mode,
|
|
model=manager.model,
|
|
parallel_mode=manager.parallel_mode,
|
|
max_concurrency=manager.max_concurrency,
|
|
)
|
|
|
|
|
|
@router.post("/start", response_model=AgentActionResponse)
|
|
async def start_agent(
|
|
project_name: str,
|
|
request: AgentStartRequest = AgentStartRequest(),
|
|
):
|
|
"""Start the agent for a project."""
|
|
manager = get_project_manager(project_name)
|
|
|
|
# Get defaults from global settings if not provided in request
|
|
default_yolo, default_model = _get_settings_defaults()
|
|
yolo_mode = request.yolo_mode if request.yolo_mode is not None else default_yolo
|
|
model = request.model if request.model else default_model
|
|
parallel_mode = request.parallel_mode or False
|
|
max_concurrency = request.max_concurrency
|
|
|
|
success, message = await manager.start(
|
|
yolo_mode=yolo_mode,
|
|
model=model,
|
|
parallel_mode=parallel_mode,
|
|
max_concurrency=max_concurrency,
|
|
)
|
|
|
|
return AgentActionResponse(
|
|
success=success,
|
|
status=manager.status,
|
|
message=message,
|
|
)
|
|
|
|
|
|
@router.post("/stop", response_model=AgentActionResponse)
|
|
async def stop_agent(project_name: str):
|
|
"""Stop the agent for a project."""
|
|
manager = get_project_manager(project_name)
|
|
|
|
success, message = await manager.stop()
|
|
|
|
return AgentActionResponse(
|
|
success=success,
|
|
status=manager.status,
|
|
message=message,
|
|
)
|
|
|
|
|
|
@router.post("/pause", response_model=AgentActionResponse)
|
|
async def pause_agent(project_name: str):
|
|
"""Pause the agent for a project."""
|
|
manager = get_project_manager(project_name)
|
|
|
|
success, message = await manager.pause()
|
|
|
|
return AgentActionResponse(
|
|
success=success,
|
|
status=manager.status,
|
|
message=message,
|
|
)
|
|
|
|
|
|
@router.post("/resume", response_model=AgentActionResponse)
|
|
async def resume_agent(project_name: str):
|
|
"""Resume a paused agent."""
|
|
manager = get_project_manager(project_name)
|
|
|
|
success, message = await manager.resume()
|
|
|
|
return AgentActionResponse(
|
|
success=success,
|
|
status=manager.status,
|
|
message=message,
|
|
)
|