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()