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