a digital person for bluesky
at main 5.6 kB view raw
1#!/usr/bin/env python3 2""" 3Multi-bot launcher for running all 5 Void agents simultaneously with aggregated logs. 4Usage: python run_bots.py [bsky.py arguments...] 5Example: python run_bots.py --synthesis-interval 0 --no-git 6 7Bots: void, herald, archivist, grunk, ezra 8""" 9 10import subprocess 11import threading 12import signal 13import sys 14import os 15from typing import List 16 17# ANSI color codes for each bot 18COLORS = { 19 'void': '\033[91m', # Red 20 'herald': '\033[94m', # Blue 21 'archivist': '\033[92m', # Green 22 'grunk': '\033[93m', # Yellow 23 'ezra': '\033[95m', # Magenta 24} 25RESET = '\033[0m' 26BOLD = '\033[1m' 27 28# Bot names and their config files 29BOTS = { 30 'void': 'configs/config.yaml', 31 'herald': 'configs/herald.yaml', 32 'archivist': 'configs/archivist.yaml', 33 'grunk': 'configs/grunk.yaml', 34 'ezra': 'configs/ezra.yaml', 35} 36 37# Track all running processes 38processes: List[subprocess.Popen] = [] 39shutdown_flag = threading.Event() 40shutdown_in_progress = threading.Lock() 41 42 43def stream_output(proc: subprocess.Popen, bot_name: str, stream_name: str): 44 """Read from a process stream and print with colored prefix.""" 45 stream = proc.stdout if stream_name == 'stdout' else proc.stderr 46 color = COLORS.get(bot_name, '') 47 prefix = f"{color}{BOLD}[{bot_name}]{RESET} " 48 49 try: 50 for line in iter(stream.readline, b''): 51 if shutdown_flag.is_set(): 52 break 53 try: 54 decoded = line.decode('utf-8').rstrip() 55 if decoded: # Only print non-empty lines 56 print(f"{prefix}{decoded}", flush=True) 57 except UnicodeDecodeError: 58 # Handle binary output gracefully 59 print(f"{prefix}[binary output]", flush=True) 60 except Exception as e: 61 if not shutdown_flag.is_set(): 62 print(f"{prefix}Error reading {stream_name}: {e}", flush=True) 63 finally: 64 stream.close() 65 66 67def shutdown_handler(signum, frame): 68 """Handle shutdown signals gracefully.""" 69 # Prevent re-entry if already shutting down 70 if not shutdown_in_progress.acquire(blocking=False): 71 # Already shutting down, force kill on second Ctrl+C 72 print(f"\n{BOLD}⚡ Force killing all processes...{RESET}", flush=True) 73 for proc in processes: 74 if proc.poll() is None: 75 try: 76 proc.kill() 77 except: 78 pass 79 sys.exit(1) 80 81 try: 82 print(f"\n{BOLD}🛑 Shutting down all bots...{RESET}", flush=True) 83 shutdown_flag.set() 84 85 # Send SIGTERM to all processes 86 for proc in processes: 87 if proc.poll() is None: # Still running 88 try: 89 proc.terminate() 90 except Exception as e: 91 print(f"Error terminating process: {e}", flush=True) 92 93 # Wait for graceful shutdown (up to 3 seconds each) 94 for i, proc in enumerate(processes): 95 try: 96 proc.wait(timeout=3) 97 except subprocess.TimeoutExpired: 98 print(f"Process {i} didn't exit gracefully, force killing...", flush=True) 99 proc.kill() 100 try: 101 proc.wait(timeout=1) 102 except: 103 pass 104 105 print(f"{BOLD}✅ All bots stopped{RESET}", flush=True) 106 finally: 107 sys.exit(0) 108 109 110def main(): 111 """Launch all bots with aggregated output.""" 112 # Set up signal handlers 113 signal.signal(signal.SIGINT, shutdown_handler) 114 signal.signal(signal.SIGTERM, shutdown_handler) 115 116 # Get command-line arguments to pass to each bot 117 bot_args = sys.argv[1:] 118 119 print(f"{BOLD}🚀 Starting {len(BOTS)} bots...{RESET}", flush=True) 120 if bot_args: 121 print(f"{BOLD}Arguments: {' '.join(bot_args)}{RESET}", flush=True) 122 print() 123 124 # Start each bot 125 threads = [] 126 for bot_name, config_file in BOTS.items(): 127 cmd = ['python', 'bsky.py', '--config', config_file] + bot_args 128 129 try: 130 proc = subprocess.Popen( 131 cmd, 132 stdout=subprocess.PIPE, 133 stderr=subprocess.PIPE, 134 ) 135 processes.append(proc) 136 137 # Start threads to read stdout and stderr 138 stdout_thread = threading.Thread( 139 target=stream_output, 140 args=(proc, bot_name, 'stdout'), 141 daemon=True 142 ) 143 stderr_thread = threading.Thread( 144 target=stream_output, 145 args=(proc, bot_name, 'stderr'), 146 daemon=True 147 ) 148 149 stdout_thread.start() 150 stderr_thread.start() 151 threads.append(stdout_thread) 152 threads.append(stderr_thread) 153 154 color = COLORS.get(bot_name, '') 155 print(f"{color}{BOLD}[{bot_name}]{RESET} Started (PID: {proc.pid})", flush=True) 156 157 except Exception as e: 158 print(f"Failed to start {bot_name}: {e}", flush=True) 159 160 print() 161 print(f"{BOLD}📡 Monitoring {len(processes)} bots. Press Ctrl+C to stop all.{RESET}", flush=True) 162 print() 163 164 # Wait for all processes to complete (or until interrupted) 165 try: 166 for proc in processes: 167 proc.wait() 168 except KeyboardInterrupt: 169 # Signal handler will take care of cleanup 170 pass 171 172 # If we get here naturally (all processes exited), clean up 173 if not shutdown_flag.is_set(): 174 print(f"\n{BOLD}All bots have exited.{RESET}", flush=True) 175 shutdown_flag.set() 176 177 178if __name__ == '__main__': 179 main()