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