""" Debugger API — pause / resume / step an in-flight pipeline. The engine is set by main.py on startup via `set_engine(...)`. If no engine is registered (e.g. when running the mock server), endpoints return a structured error rather than crashing. """ from fastapi import APIRouter from typing import Any, Dict, Optional import threading router = APIRouter(prefix="/debugger", tags=["debugger"]) # F-15: guards for module-level mutable state used from async handlers. # Multiple WS clients can call pause/resume/step concurrently. _engine_lock = threading.Lock() _state_lock = threading.Lock() # Single in-memory engine reference (set by main.py) _ENGINE: Optional[Any] = None _DEBUG_STATE: Dict[str, Any] = { "paused": False, "current_step": 0, "history": [], } def set_engine(engine: Any) -> None: global _ENGINE with _engine_lock: _ENGINE = engine def get_engine() -> Optional[Any]: with _engine_lock: return _ENGINE # ----------------------------- # RESUME # ----------------------------- @router.post("/resume") def resume() -> Dict[str, Any]: with _state_lock: _DEBUG_STATE["paused"] = False eng = get_engine() if eng and hasattr(eng, "resume"): eng.resume() return {"state": "running"} # ----------------------------- # PAUSE # ----------------------------- @router.post("/pause") def pause() -> Dict[str, Any]: with _state_lock: _DEBUG_STATE["paused"] = True eng = get_engine() if eng and hasattr(eng, "pause"): eng.pause() return {"state": "paused"} # ----------------------------- # STEP # ----------------------------- @router.post("/step") def step() -> Dict[str, Any]: eng = get_engine() if eng and hasattr(eng, "step"): eng.step() with _state_lock: _DEBUG_STATE["current_step"] += 1 step_n = _DEBUG_STATE["current_step"] return {"state": "stepped", "step": step_n} # ----------------------------- # STEP BACK (replay-only) # ----------------------------- @router.post("/step-back") def step_back() -> Dict[str, Any]: with _state_lock: if _DEBUG_STATE["current_step"] > 0: _DEBUG_STATE["current_step"] -= 1 step_n = _DEBUG_STATE["current_step"] return {"state": "stepped_back", "step": step_n} # ----------------------------- # STATE # ----------------------------- @router.get("/state") def state() -> Dict[str, Any]: eng = get_engine() active = bool(eng and hasattr(eng, "is_active") and eng.is_active()) with _state_lock: snapshot = { "paused": _DEBUG_STATE["paused"], "current_step": _DEBUG_STATE["current_step"], "engine_attached": eng is not None, "engine_active": active, } return snapshot # ----------------------------- # Legacy wrapper for api.py facade # ----------------------------- def debugger_endpoint(session_id: str, registry: Any = None, bus: Any = None) -> Dict[str, Any]: """Legacy — return current debugger state for a session.""" with _state_lock: return { "session_id": session_id, "paused": _DEBUG_STATE["paused"], "current_step": _DEBUG_STATE["current_step"], "engine_attached": _ENGINE is not None, }