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