a digital person for bluesky
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()