+154
run_bots.py
+154
run_bots.py
···
1
+
#!/usr/bin/env python3
2
+
"""
3
+
Multi-bot launcher for running all Void agents simultaneously with aggregated logs.
4
+
Usage: python run_bots.py [bsky.py arguments...]
5
+
Example: python run_bots.py --synthesis-interval 0 --no-git
6
+
"""
7
+
8
+
import subprocess
9
+
import threading
10
+
import signal
11
+
import sys
12
+
import os
13
+
from typing import List
14
+
15
+
# ANSI color codes for each bot
16
+
COLORS = {
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
+
}
23
+
RESET = '\033[0m'
24
+
BOLD = '\033[1m'
25
+
26
+
# List of bot configs to run
27
+
BOTS = ['herald', 'archivist', 'grunk', 'ezra', 'blank']
28
+
29
+
# Track all running processes
30
+
processes: List[subprocess.Popen] = []
31
+
shutdown_flag = threading.Event()
32
+
33
+
34
+
def 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
+
58
+
def 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
+
84
+
def 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
+
153
+
if __name__ == '__main__':
154
+
main()