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