import threading import time from pr.tools.interactive_control import ( get_session_status, list_active_sessions, read_session_output, ) class AutonomousInteractions: def __init__(self, interaction_interval=10.0): self.interaction_interval = interaction_interval self.active = False self.interaction_thread = None self.llm_callback = None self.last_check_time = 0 def start(self, llm_callback=None): """Start the autonomous interaction loop.""" self.llm_callback = llm_callback if self.interaction_thread is None: self.active = True self.interaction_thread = threading.Thread( target=self._interaction_loop, daemon=True ) self.interaction_thread.start() def stop(self): """Stop the autonomous interaction loop.""" self.active = False if self.interaction_thread: self.interaction_thread.join(timeout=2) def _interaction_loop(self): """Main loop for autonomous interactions with background processes.""" while self.active: try: current_time = time.time() if current_time - self.last_check_time >= self.interaction_interval: self._check_sessions_and_notify() self.last_check_time = current_time time.sleep(1) # Check every second for shutdown except Exception as e: print(f"Error in autonomous interaction loop: {e}") time.sleep(self.interaction_interval) def _check_sessions_and_notify(self): """Check active sessions and determine if LLM notification is needed.""" try: sessions = list_active_sessions() if not sessions: return # No active sessions sessions_needing_attention = self._identify_sessions_needing_attention( sessions ) if sessions_needing_attention and self.llm_callback: # Format session updates for LLM updates = self._format_session_updates(sessions_needing_attention) self.llm_callback(updates) except Exception as e: print(f"Error checking sessions: {e}") def _identify_sessions_needing_attention(self, sessions): """Identify which sessions need LLM attention based on various criteria.""" needing_attention = [] for session_name, session_data in sessions.items(): metadata = session_data["metadata"] output_summary = session_data["output_summary"] # Criteria for needing attention: # 1. Recent output activity time_since_activity = time.time() - metadata.get("last_activity", 0) if time_since_activity < 30: # Activity in last 30 seconds needing_attention.append(session_name) continue # 2. High output volume (potential completion or error) total_lines = ( output_summary["stdout_lines"] + output_summary["stderr_lines"] ) if total_lines > 50: # Arbitrary threshold needing_attention.append(session_name) continue # 3. Long-running sessions that might need intervention session_age = time.time() - metadata.get("start_time", 0) if ( session_age > 300 and time_since_activity > 60 ): # 5+ minutes old, inactive for 1+ minute needing_attention.append(session_name) continue # 4. Sessions that appear to be waiting for input if self._session_looks_stuck(session_name, session_data): needing_attention.append(session_name) continue return needing_attention def _session_looks_stuck(self, session_name, session_data): """Determine if a session appears to be stuck waiting for input.""" metadata = session_data["metadata"] # Check if process is still running status = get_session_status(session_name) if not status or not status.get("is_active", False): return False time_since_activity = time.time() - metadata.get("last_activity", 0) interaction_count = metadata.get("interaction_count", 0) # If running for a while but no interactions, might be waiting session_age = time.time() - metadata.get("start_time", 0) if session_age > 60 and interaction_count == 0 and time_since_activity > 30: return True # If had interactions but been quiet for a while if interaction_count > 0 and time_since_activity > 120: # 2 minutes return True return False def _format_session_updates(self, session_names): """Format session information for LLM consumption.""" updates = { "type": "background_session_updates", "timestamp": time.time(), "sessions": {}, } for session_name in session_names: status = get_session_status(session_name) if status: # Get recent output (last 20 lines) try: recent_output = read_session_output(session_name, lines=20) except: recent_output = {"stdout": "", "stderr": ""} updates["sessions"][session_name] = { "status": status, "recent_output": recent_output, "summary": self._create_session_summary(status, recent_output), } return updates def _create_session_summary(self, status, recent_output): """Create a human-readable summary of session status.""" summary_parts = [] process_type = status.get("metadata", {}).get("process_type", "unknown") summary_parts.append(f"Type: {process_type}") is_active = status.get("is_active", False) summary_parts.append(f"Status: {'Active' if is_active else 'Inactive'}") if is_active and "pid" in status: summary_parts.append(f"PID: {status['pid']}") age = time.time() - status.get("metadata", {}).get("start_time", 0) summary_parts.append(f"Age: {age:.1f}s") output_lines = len(recent_output.get("stdout", "").split("\n")) + len( recent_output.get("stderr", "").split("\n") ) summary_parts.append(f"Recent output: {output_lines} lines") interaction_count = status.get("metadata", {}).get("interaction_count", 0) summary_parts.append(f"Interactions: {interaction_count}") return " | ".join(summary_parts) # Global autonomous interactions instance _global_autonomous = None def get_global_autonomous(): """Get the global autonomous interactions instance.""" global _global_autonomous return _global_autonomous def start_global_autonomous(llm_callback=None): """Start global autonomous interactions.""" global _global_autonomous if _global_autonomous is None: _global_autonomous = AutonomousInteractions() _global_autonomous.start(llm_callback) return _global_autonomous def stop_global_autonomous(): """Stop global autonomous interactions.""" global _global_autonomous if _global_autonomous: _global_autonomous.stop() _global_autonomous = None