mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-01 23:13:36 +00:00
Token reduction (~40% per session, ~2.3M fewer tokens per 200-feature project): - Agent-type-specific tool lists: coding 9, testing 5, init 5 (was 19 for all) - Right-sized max_turns: coding 300, testing 100 (was 1000 for all) - Trimmed coding prompt template (~150 lines removed) - Streamlined testing prompt with batch support - YOLO mode now strips browser testing instructions from prompt - Added Grep, WebFetch, WebSearch to expand project session Performance improvements: - Rate limit retries start at ~15s with jitter (was fixed 60s) - Post-spawn delay reduced to 0.5s (was 2s) - Orchestrator consolidated to 1 DB query per loop (was 5-7) - Testing agents batch 3 features per session (was 1) - Smart context compaction preserves critical state, discards noise Bug fixes: - Removed ghost feature_release_testing MCP tool (wasted tokens every test session) - Forward all 9 Vertex AI env vars to chat sessions (was missing 3) - Fix DetachedInstanceError risk in test batch ORM access - Prevent duplicate testing of same features in parallel mode Code deduplication: - _get_project_path(): 9 copies -> 1 shared utility (project_helpers.py) - validate_project_name(): 9 copies -> 2 variants in 1 file (validation.py) - ROOT_DIR: 10 copies -> 1 definition (chat_constants.py) - API_ENV_VARS: 4 copies -> 1 source of truth (env_constants.py) Security hardening: - Unified sensitive directory blocklist (14 dirs, was two divergent lists) - Cached get_blocked_paths() for O(1) directory listing checks - Terminal security warning when ALLOW_REMOTE=1 exposes WebSocket - 20 new security tests for EXTRA_READ_PATHS blocking - Extracted _validate_command_list() and _validate_pkill_processes() helpers Type safety: - 87 mypy errors -> 0 across 58 source files - Installed types-PyYAML for proper yaml stub types - Fixed SQLAlchemy Column[T] coercions across all routers Dead code removed: - 13 files deleted (~2,679 lines): unused UI components, debug logs, outdated docs - 7 unused npm packages removed (Radix UI components with 0 imports) - AgentAvatar.tsx reduced from 615 -> 119 lines (SVGs extracted to mascotData.tsx) New CLI options: - --testing-batch-size (1-5) for parallel mode test batching - --testing-feature-ids for direct multi-feature testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
167 lines
5.2 KiB
Python
167 lines
5.2 KiB
Python
"""
|
|
Agent Router
|
|
============
|
|
|
|
API endpoints for agent control (start/stop/pause/resume).
|
|
Uses project registry for path lookups.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from ..schemas import AgentActionResponse, AgentStartRequest, AgentStatus
|
|
from ..services.chat_constants import ROOT_DIR
|
|
from ..services.process_manager import get_manager
|
|
from ..utils.project_helpers import get_project_path as _get_project_path
|
|
from ..utils.validation import validate_project_name
|
|
|
|
|
|
def _get_settings_defaults() -> tuple[bool, str, int]:
|
|
"""Get defaults from global settings.
|
|
|
|
Returns:
|
|
Tuple of (yolo_mode, model, testing_agent_ratio)
|
|
"""
|
|
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)
|
|
|
|
# Parse testing agent settings with defaults
|
|
try:
|
|
testing_agent_ratio = int(settings.get("testing_agent_ratio", "1"))
|
|
except (ValueError, TypeError):
|
|
testing_agent_ratio = 1
|
|
|
|
return yolo_mode, model, testing_agent_ratio
|
|
|
|
|
|
router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
|
|
|
|
|
|
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.isoformat() if manager.started_at else None,
|
|
yolo_mode=manager.yolo_mode,
|
|
model=manager.model,
|
|
parallel_mode=manager.parallel_mode,
|
|
max_concurrency=manager.max_concurrency,
|
|
testing_agent_ratio=manager.testing_agent_ratio,
|
|
)
|
|
|
|
|
|
@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, default_testing_ratio = _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
|
|
max_concurrency = request.max_concurrency or 1
|
|
testing_agent_ratio = request.testing_agent_ratio if request.testing_agent_ratio is not None else default_testing_ratio
|
|
|
|
success, message = await manager.start(
|
|
yolo_mode=yolo_mode,
|
|
model=model,
|
|
max_concurrency=max_concurrency,
|
|
testing_agent_ratio=testing_agent_ratio,
|
|
)
|
|
|
|
# Notify scheduler of manual start (to prevent auto-stop during scheduled window)
|
|
if success:
|
|
from ..services.scheduler_service import get_scheduler
|
|
project_dir = _get_project_path(project_name)
|
|
if project_dir:
|
|
get_scheduler().notify_manual_start(project_name, project_dir)
|
|
|
|
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()
|
|
|
|
# Notify scheduler of manual stop (to prevent auto-start during scheduled window)
|
|
if success:
|
|
from ..services.scheduler_service import get_scheduler
|
|
project_dir = _get_project_path(project_name)
|
|
if project_dir:
|
|
get_scheduler().notify_manual_stop(project_name, project_dir)
|
|
|
|
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,
|
|
)
|