import os
import logging
from typing import List, Tuple, Dict, Any, Optional
from sqlalchemy.orm import Session
from app.db import models
class SafetyGuard:
"""Handles loop detection, cancellation checks, and task watchdogs."""
def __init__(self, db: Optional[Session] = None, session_id: Optional[int] = None):
self.db = db
self.session_id = session_id
self.action_history: List[Tuple] = []
def check_cancellation(self) -> bool:
"""Returns True if user has requested cancellation."""
if not self.db or not self.session_id:
return False
try:
# Fresh query to bypass stale cache
session = self.db.query(models.Session).filter(models.Session.id == self.session_id).first()
if session:
self.db.refresh(session)
return session.is_cancelled
except Exception as e:
logging.warning(f"[SafetyGuard] Cancellation check failed: {e}")
return False
def detect_loop(self, tool_calls: List[Any]) -> bool:
"""Returns True if the exact same tool set repeats 3 times sequentially."""
current_sig = tuple(sorted([(tc.function.name, tc.function.arguments) for tc in tool_calls]))
self.action_history.append(current_sig)
if len(self.action_history) > 10:
self.action_history.pop(0)
return self.action_history.count(current_sig) >= 3
def should_activate_watchdog(self, assistant: Any, sync_workspace_id: str) -> bool:
"""Checks if .ai_todo.md has unchecked items."""
if not assistant or not assistant.mirror or not sync_workspace_id:
return False
try:
workspace = assistant.mirror.get_workspace_path(sync_workspace_id)
todo_path = os.path.join(workspace, ".ai_todo.md")
if os.path.exists(todo_path):
with open(todo_path, "r") as f:
content = f.read()
return "[ ]" in content
except Exception as e:
logging.warning(f"[SafetyGuard] Watchdog error: {e}")
return False