massgen 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (76) hide show
  1. massgen/__init__.py +94 -0
  2. massgen/agent_config.py +507 -0
  3. massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
  4. massgen/backend/Function calling openai responses.md +1161 -0
  5. massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
  6. massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
  7. massgen/backend/__init__.py +25 -0
  8. massgen/backend/base.py +180 -0
  9. massgen/backend/chat_completions.py +228 -0
  10. massgen/backend/claude.py +661 -0
  11. massgen/backend/gemini.py +652 -0
  12. massgen/backend/grok.py +187 -0
  13. massgen/backend/response.py +397 -0
  14. massgen/chat_agent.py +440 -0
  15. massgen/cli.py +686 -0
  16. massgen/configs/README.md +293 -0
  17. massgen/configs/creative_team.yaml +53 -0
  18. massgen/configs/gemini_4o_claude.yaml +31 -0
  19. massgen/configs/news_analysis.yaml +51 -0
  20. massgen/configs/research_team.yaml +51 -0
  21. massgen/configs/single_agent.yaml +18 -0
  22. massgen/configs/single_flash2.5.yaml +44 -0
  23. massgen/configs/technical_analysis.yaml +51 -0
  24. massgen/configs/three_agents_default.yaml +31 -0
  25. massgen/configs/travel_planning.yaml +51 -0
  26. massgen/configs/two_agents.yaml +39 -0
  27. massgen/frontend/__init__.py +20 -0
  28. massgen/frontend/coordination_ui.py +945 -0
  29. massgen/frontend/displays/__init__.py +24 -0
  30. massgen/frontend/displays/base_display.py +83 -0
  31. massgen/frontend/displays/rich_terminal_display.py +3497 -0
  32. massgen/frontend/displays/simple_display.py +93 -0
  33. massgen/frontend/displays/terminal_display.py +381 -0
  34. massgen/frontend/logging/__init__.py +9 -0
  35. massgen/frontend/logging/realtime_logger.py +197 -0
  36. massgen/message_templates.py +431 -0
  37. massgen/orchestrator.py +1222 -0
  38. massgen/tests/__init__.py +10 -0
  39. massgen/tests/multi_turn_conversation_design.md +214 -0
  40. massgen/tests/multiturn_llm_input_analysis.md +189 -0
  41. massgen/tests/test_case_studies.md +113 -0
  42. massgen/tests/test_claude_backend.py +310 -0
  43. massgen/tests/test_grok_backend.py +160 -0
  44. massgen/tests/test_message_context_building.py +293 -0
  45. massgen/tests/test_rich_terminal_display.py +378 -0
  46. massgen/tests/test_v3_3agents.py +117 -0
  47. massgen/tests/test_v3_simple.py +216 -0
  48. massgen/tests/test_v3_three_agents.py +272 -0
  49. massgen/tests/test_v3_two_agents.py +176 -0
  50. massgen/utils.py +79 -0
  51. massgen/v1/README.md +330 -0
  52. massgen/v1/__init__.py +91 -0
  53. massgen/v1/agent.py +605 -0
  54. massgen/v1/agents.py +330 -0
  55. massgen/v1/backends/gemini.py +584 -0
  56. massgen/v1/backends/grok.py +410 -0
  57. massgen/v1/backends/oai.py +571 -0
  58. massgen/v1/cli.py +351 -0
  59. massgen/v1/config.py +169 -0
  60. massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
  61. massgen/v1/examples/fast_config.yaml +44 -0
  62. massgen/v1/examples/production.yaml +70 -0
  63. massgen/v1/examples/single_agent.yaml +39 -0
  64. massgen/v1/logging.py +974 -0
  65. massgen/v1/main.py +368 -0
  66. massgen/v1/orchestrator.py +1138 -0
  67. massgen/v1/streaming_display.py +1190 -0
  68. massgen/v1/tools.py +160 -0
  69. massgen/v1/types.py +245 -0
  70. massgen/v1/utils.py +199 -0
  71. massgen-0.0.3.dist-info/METADATA +568 -0
  72. massgen-0.0.3.dist-info/RECORD +76 -0
  73. massgen-0.0.3.dist-info/WHEEL +5 -0
  74. massgen-0.0.3.dist-info/entry_points.txt +2 -0
  75. massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
  76. massgen-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1190 @@
1
+ """
2
+ MassGen Streaming Display System
3
+
4
+ Provides real-time multi-region display for MassGen agents with:
5
+ - Individual agent columns showing streaming conversations
6
+ - System status panel with phase transitions and voting
7
+ - File logging for all conversations and events
8
+ """
9
+
10
+ import os
11
+ import time
12
+ import threading
13
+ import unicodedata
14
+ import sys
15
+ import re
16
+ from typing import Dict, List, Optional, Callable, Union
17
+ from datetime import datetime
18
+
19
+
20
+ class MultiRegionDisplay:
21
+ def __init__(
22
+ self,
23
+ display_enabled: bool = True,
24
+ max_lines: int = 10,
25
+ save_logs: bool = True,
26
+ answers_dir: Optional[str] = None,
27
+ ):
28
+ self.display_enabled = display_enabled
29
+ self.max_lines = max_lines
30
+ self.save_logs = save_logs
31
+ self.answers_dir = answers_dir # Path to answers directory from logging system
32
+ self.agent_outputs: Dict[int, str] = {}
33
+ self.agent_models: Dict[int, str] = {}
34
+ self.agent_statuses: Dict[int, str] = {} # Track agent statuses
35
+ self.system_messages: List[str] = []
36
+ self.start_time = time.time()
37
+ self._lock = threading.RLock() # Use reentrant lock to prevent deadlock
38
+
39
+ # MassGen-specific state tracking
40
+ self.current_phase = "collaboration"
41
+ self.vote_distribution: Dict[int, int] = {}
42
+ self.consensus_reached = False
43
+ self.representative_agent_id: Optional[int] = None
44
+ self.debate_rounds: int = 0 # Track debate rounds
45
+
46
+ # Detailed agent state tracking for display
47
+ self._agent_vote_targets: Dict[int, Optional[int]] = {}
48
+ self._agent_chat_rounds: Dict[int, int] = {}
49
+ self._agent_update_counts: Dict[int, int] = {} # Track update history count
50
+ self._agent_votes_cast: Dict[int, int] = (
51
+ {}
52
+ ) # Track number of votes cast by each agent
53
+
54
+ # Simplified, consistent border tracking
55
+ self._display_cache = None # Single cache object for all dimensions
56
+ self._last_agent_count = 0 # Track when to invalidate cache
57
+
58
+ # CRITICAL FIX: Debounced display updates to prevent race conditions
59
+ self._update_timer = None
60
+ self._update_delay = 0.1 # 100ms debounce
61
+ self._display_updating = False
62
+ self._pending_update = False
63
+
64
+ # ROBUST DISPLAY: Improved ANSI and Unicode handling
65
+ self._ansi_pattern = re.compile(
66
+ r"\x1B(?:" # ESC
67
+ r"[@-Z\\-_]" # Fe Escape sequences
68
+ r"|"
69
+ r"\["
70
+ r"[0-?]*[ -/]*[@-~]" # CSI sequences
71
+ r"|"
72
+ r"\][^\x07]*(?:\x07|\x1B\\)" # OSC sequences
73
+ r"|"
74
+ r"[PX^_][^\x1B]*\x1B\\" # Other escape sequences
75
+ r")"
76
+ )
77
+
78
+ # Initialize logging directory and files
79
+ if self.save_logs:
80
+ self._setup_logging()
81
+
82
+ def _get_terminal_width(self):
83
+ """Get terminal width with conservative fallback."""
84
+ try:
85
+ return os.get_terminal_size().columns
86
+ except:
87
+ return 120 # Safe default
88
+
89
+ def _calculate_layout(self, num_agents: int):
90
+ """
91
+ Calculate all layout dimensions in one place for consistency.
92
+ Returns: (col_width, total_width, terminal_width)
93
+ """
94
+ # Invalidate cache if agent count changed or no cache exists
95
+ if self._display_cache is None or self._last_agent_count != num_agents:
96
+
97
+ terminal_width = self._get_terminal_width()
98
+
99
+ # More conservative calculation to prevent overflow
100
+ # Each column needs: content + left border (│)
101
+ # Plus one final border (│) at the end
102
+ border_chars = num_agents + 1 # │col1│col2│col3│
103
+ safety_margin = 10 # Increased safety margin for terminal variations
104
+
105
+ available_width = terminal_width - border_chars - safety_margin
106
+ col_width = max(
107
+ 25, available_width // num_agents
108
+ ) # Minimum 25 chars per column
109
+
110
+ # Calculate actual total width used
111
+ total_width = (col_width * num_agents) + border_chars
112
+
113
+ # Final safety check - ensure we don't exceed terminal
114
+ if total_width > terminal_width - 2: # Extra 2 char safety
115
+ col_width = max(20, (terminal_width - border_chars - 4) // num_agents)
116
+ total_width = (col_width * num_agents) + border_chars
117
+
118
+ # Cache the results
119
+ self._display_cache = {
120
+ "col_width": col_width,
121
+ "total_width": total_width,
122
+ "terminal_width": terminal_width,
123
+ "num_agents": num_agents,
124
+ "border_chars": border_chars,
125
+ }
126
+ self._last_agent_count = num_agents
127
+
128
+ cache = self._display_cache
129
+ return cache["col_width"], cache["total_width"], cache["terminal_width"]
130
+
131
+ def _get_display_width(self, text: str) -> int:
132
+ """
133
+ ROBUST: Calculate the actual display width of text with proper ANSI and Unicode handling.
134
+ """
135
+ if not text:
136
+ return 0
137
+
138
+ # Remove ALL ANSI escape sequences using comprehensive regex
139
+ clean_text = self._ansi_pattern.sub("", text)
140
+
141
+ width = 0
142
+ i = 0
143
+ while i < len(clean_text):
144
+ char = clean_text[i]
145
+ char_code = ord(char)
146
+
147
+ # Handle control characters (should not contribute to width)
148
+ if char_code < 32 or char_code == 127: # Control characters
149
+ i += 1
150
+ continue
151
+
152
+ # Handle Unicode combining characters (zero-width)
153
+ if unicodedata.combining(char):
154
+ i += 1
155
+ continue
156
+
157
+ # Handle emoji and wide characters more comprehensively
158
+ char_width = self._get_char_width(char)
159
+ width += char_width
160
+ i += 1
161
+
162
+ return width
163
+
164
+ def _get_char_width(self, char: str) -> int:
165
+ """
166
+ ROBUST: Get the display width of a single character.
167
+ """
168
+ char_code = ord(char)
169
+
170
+ # ASCII printable characters
171
+ if 32 <= char_code <= 126:
172
+ return 1
173
+
174
+ # Common emoji ranges (display as width 2)
175
+ if (
176
+ # Basic emoji ranges
177
+ (0x1F600 <= char_code <= 0x1F64F) # Emoticons
178
+ or (0x1F300 <= char_code <= 0x1F5FF) # Misc symbols
179
+ or (0x1F680 <= char_code <= 0x1F6FF) # Transport
180
+ or (0x1F700 <= char_code <= 0x1F77F) # Alchemical symbols
181
+ or (0x1F780 <= char_code <= 0x1F7FF) # Geometric shapes extended
182
+ or (0x1F800 <= char_code <= 0x1F8FF) # Supplemental arrows-C
183
+ or (0x1F900 <= char_code <= 0x1F9FF) # Supplemental symbols
184
+ or (0x1FA00 <= char_code <= 0x1FA6F) # Chess symbols
185
+ or (0x1FA70 <= char_code <= 0x1FAFF) # Symbols and pictographs extended-A
186
+ or (0x1F1E6 <= char_code <= 0x1F1FF) # Regional indicator symbols (flags)
187
+ or
188
+ # Misc symbols and dingbats
189
+ (0x2600 <= char_code <= 0x26FF) # Misc symbols
190
+ or (0x2700 <= char_code <= 0x27BF) # Dingbats
191
+ or (0x1F0A0 <= char_code <= 0x1F0FF) # Playing cards
192
+ or
193
+ # Mathematical symbols
194
+ (0x1F100 <= char_code <= 0x1F1FF) # Enclosed alphanumeric supplement
195
+ ):
196
+ return 2
197
+
198
+ # Use Unicode East Asian Width property for CJK characters
199
+ east_asian_width = unicodedata.east_asian_width(char)
200
+ if east_asian_width in ("F", "W"): # Fullwidth or Wide
201
+ return 2
202
+ elif east_asian_width in ("N", "Na", "H"): # Narrow, Not assigned, Halfwidth
203
+ return 1
204
+ elif east_asian_width == "A": # Ambiguous - default to 1 for safety
205
+ return 1
206
+
207
+ # Default to 1 for unknown characters
208
+ return 1
209
+
210
+ def _preserve_ansi_truncate(self, text: str, max_width: int) -> str:
211
+ """
212
+ ROBUST: Truncate text while preserving ANSI color codes and handling wide characters.
213
+ """
214
+ if max_width <= 0:
215
+ return ""
216
+
217
+ if max_width <= 1:
218
+ return "…"
219
+
220
+ # Split text into ANSI codes and regular text segments
221
+ segments = self._ansi_pattern.split(text)
222
+ ansi_codes = self._ansi_pattern.findall(text)
223
+
224
+ result = ""
225
+ current_width = 0
226
+ ansi_index = 0
227
+
228
+ for i, segment in enumerate(segments):
229
+ # Add ANSI code if this isn't the first segment
230
+ if i > 0 and ansi_index < len(ansi_codes):
231
+ result += ansi_codes[ansi_index]
232
+ ansi_index += 1
233
+
234
+ # Process regular text segment
235
+ for char in segment:
236
+ char_width = self._get_char_width(char)
237
+
238
+ # Check if we can fit this character
239
+ if (
240
+ current_width + char_width > max_width - 1
241
+ ): # Save space for ellipsis
242
+ # Try to add ellipsis if possible
243
+ if current_width < max_width:
244
+ result += "…"
245
+ return result
246
+
247
+ result += char
248
+ current_width += char_width
249
+
250
+ return result
251
+
252
+ def _pad_to_width(self, text: str, target_width: int, align: str = "left") -> str:
253
+ """
254
+ ROBUST: Pad text to exact target width with proper ANSI and Unicode handling.
255
+ """
256
+ if target_width <= 0:
257
+ return ""
258
+
259
+ current_width = self._get_display_width(text)
260
+
261
+ # Truncate if too long
262
+ if current_width > target_width:
263
+ text = self._preserve_ansi_truncate(text, target_width)
264
+ current_width = self._get_display_width(text)
265
+
266
+ # Calculate padding needed
267
+ padding = target_width - current_width
268
+ if padding <= 0:
269
+ return text
270
+
271
+ # Apply padding based on alignment
272
+ if align == "center":
273
+ left_pad = padding // 2
274
+ right_pad = padding - left_pad
275
+ return " " * left_pad + text + " " * right_pad
276
+ elif align == "right":
277
+ return " " * padding + text
278
+ else: # left
279
+ return text + " " * padding
280
+
281
+ def _create_bordered_line(self, content_parts: List[str], total_width: int) -> str:
282
+ """
283
+ ROBUST: Create a single bordered line with guaranteed correct width.
284
+ """
285
+ # Ensure all content parts are exactly the right width
286
+ validated_parts = []
287
+ for part in content_parts:
288
+ if self._get_display_width(part) != self._display_cache["col_width"]:
289
+ # Re-pad if width is incorrect
290
+ part = self._pad_to_width(
291
+ part, self._display_cache["col_width"], "left"
292
+ )
293
+ validated_parts.append(part)
294
+
295
+ # Join content with borders: │content1│content2│content3│
296
+ line = "│" + "│".join(validated_parts) + "│"
297
+
298
+ # Final width validation
299
+ actual_width = self._get_display_width(line)
300
+ expected_width = total_width
301
+
302
+ if actual_width != expected_width:
303
+ # Emergency fix - truncate or pad the entire line
304
+ if actual_width > expected_width:
305
+ # Strip ANSI codes and truncate
306
+ clean_line = self._ansi_pattern.sub("", line)
307
+ if len(clean_line) > expected_width:
308
+ clean_line = clean_line[: expected_width - 1] + "│"
309
+ line = clean_line
310
+ else:
311
+ # Pad to reach exact width
312
+ line += " " * (expected_width - actual_width)
313
+
314
+ return line
315
+
316
+ def _create_system_bordered_line(self, content: str, total_width: int) -> str:
317
+ """
318
+ ROBUST: Create a system section line with borders.
319
+ """
320
+ content_width = total_width - 2 # Account for │ on each side
321
+ if content_width <= 0:
322
+ return "│" + " " * max(0, total_width - 2) + "│"
323
+
324
+ padded_content = self._pad_to_width(content, content_width, "left")
325
+ line = f"│{padded_content}│"
326
+
327
+ # Validate final width
328
+ actual_width = self._get_display_width(line)
329
+ if actual_width != total_width:
330
+ # Emergency padding
331
+ if actual_width < total_width:
332
+ line += " " * (total_width - actual_width)
333
+ elif actual_width > total_width:
334
+ # Strip ANSI and truncate
335
+ clean_line = self._ansi_pattern.sub("", line)
336
+ if len(clean_line) > total_width:
337
+ clean_line = clean_line[: total_width - 1] + "│"
338
+ line = clean_line
339
+
340
+ return line
341
+
342
+ def _invalidate_display_cache(self):
343
+ """Reset display cache when terminal is resized."""
344
+ self._display_cache = None
345
+
346
+ def cleanup(self):
347
+ """Clean up resources when display is no longer needed."""
348
+ with self._lock:
349
+ if self._update_timer:
350
+ self._update_timer.cancel()
351
+ self._update_timer = None
352
+ self._pending_update = False
353
+ self._display_updating = False
354
+
355
+ def _clear_terminal_atomic(self):
356
+ """Atomically clear terminal using proper ANSI sequences."""
357
+ try:
358
+ # Use ANSI escape sequences for atomic terminal clearing
359
+ # This is more reliable than os.system('clear')
360
+ sys.stdout.write("\033[2J") # Clear entire screen
361
+ sys.stdout.write("\033[H") # Move cursor to home position
362
+ sys.stdout.flush() # Ensure immediate execution
363
+ except Exception:
364
+ # Fallback to os.system if ANSI sequences fail
365
+ try:
366
+ os.system("clear" if os.name == "posix" else "cls")
367
+ except Exception:
368
+ pass # Silent fallback if all clearing methods fail
369
+
370
+ def _schedule_display_update(self):
371
+ """Schedule a debounced display update to prevent rapid refreshes."""
372
+ with self._lock:
373
+ if self._update_timer:
374
+ self._update_timer.cancel()
375
+
376
+ # Set pending update flag
377
+ self._pending_update = True
378
+
379
+ # Schedule update after delay
380
+ self._update_timer = threading.Timer(
381
+ self._update_delay, self._execute_display_update
382
+ )
383
+ self._update_timer.start()
384
+
385
+ def _execute_display_update(self):
386
+ """Execute the actual display update."""
387
+ with self._lock:
388
+ if not self._pending_update:
389
+ return
390
+
391
+ # Prevent concurrent updates
392
+ if self._display_updating:
393
+ # Reschedule if another update is in progress
394
+ self._update_timer = threading.Timer(
395
+ self._update_delay, self._execute_display_update
396
+ )
397
+ self._update_timer.start()
398
+ return
399
+
400
+ self._display_updating = True
401
+ self._pending_update = False
402
+
403
+ try:
404
+ self._update_display_immediate()
405
+ finally:
406
+ with self._lock:
407
+ self._display_updating = False
408
+
409
+ def set_agent_model(self, agent_id: int, model_name: str):
410
+ """Set the model name for a specific agent."""
411
+ with self._lock:
412
+ self.agent_models[agent_id] = model_name
413
+ # Ensure agent appears in display even if no content yet
414
+ if agent_id not in self.agent_outputs:
415
+ self.agent_outputs[agent_id] = ""
416
+
417
+ def update_agent_status(self, agent_id: int, status: str):
418
+ """Update agent status (working, voted, failed)."""
419
+ with self._lock:
420
+ old_status = self.agent_statuses.get(agent_id, "unknown")
421
+ self.agent_statuses[agent_id] = status
422
+
423
+ # Ensure agent appears in display even if no content yet
424
+ if agent_id not in self.agent_outputs:
425
+ self.agent_outputs[agent_id] = ""
426
+
427
+ # Status emoji mapping for system messages
428
+ status_change_emoji = {
429
+ "working": "🔄",
430
+ "voted": "✅",
431
+ "failed": "❌",
432
+ "unknown": "❓",
433
+ }
434
+
435
+ # Log status change with emoji
436
+ old_emoji = status_change_emoji.get(old_status, "❓")
437
+ new_emoji = status_change_emoji.get(status, "❓")
438
+ status_msg = (
439
+ f"{old_emoji}→{new_emoji} Agent {agent_id}: {old_status} → {status}"
440
+ )
441
+ self.add_system_message(status_msg)
442
+
443
+ def update_phase(self, old_phase: str, new_phase: str):
444
+ """Update system phase."""
445
+ with self._lock:
446
+ self.current_phase = new_phase
447
+ phase_msg = f"Phase: {old_phase} → {new_phase}"
448
+ self.add_system_message(phase_msg)
449
+
450
+ def update_vote_distribution(self, vote_dist: Dict[int, int]):
451
+ """Update vote distribution."""
452
+ with self._lock:
453
+ self.vote_distribution = vote_dist.copy()
454
+
455
+ def update_consensus_status(
456
+ self, representative_id: int, vote_dist: Dict[int, int]
457
+ ):
458
+ """Update when consensus is reached."""
459
+ with self._lock:
460
+ self.consensus_reached = True
461
+ self.representative_agent_id = representative_id
462
+ self.vote_distribution = vote_dist.copy()
463
+
464
+ consensus_msg = f"🎉 CONSENSUS REACHED! Agent {representative_id} selected as representative"
465
+ self.add_system_message(consensus_msg)
466
+
467
+ def reset_consensus(self):
468
+ """Reset consensus state for new debate round."""
469
+ with self._lock:
470
+ self.consensus_reached = False
471
+ self.representative_agent_id = None
472
+ self.vote_distribution.clear()
473
+
474
+ def update_agent_vote_target(self, agent_id: int, target_id: Optional[int]):
475
+ """Update which agent this agent voted for."""
476
+ with self._lock:
477
+ self._agent_vote_targets[agent_id] = target_id
478
+
479
+ def update_agent_chat_round(self, agent_id: int, round_num: int):
480
+ """Update the chat round for an agent."""
481
+ with self._lock:
482
+ self._agent_chat_rounds[agent_id] = round_num
483
+
484
+ def update_agent_update_count(self, agent_id: int, count: int):
485
+ """Update the update count for an agent."""
486
+ with self._lock:
487
+ self._agent_update_counts[agent_id] = count
488
+
489
+ def update_agent_votes_cast(self, agent_id: int, votes_cast: int):
490
+ """Update the number of votes cast by an agent."""
491
+ with self._lock:
492
+ self._agent_votes_cast[agent_id] = votes_cast
493
+
494
+ def update_debate_rounds(self, rounds: int):
495
+ """Update the debate rounds count."""
496
+ with self._lock:
497
+ self.debate_rounds = rounds
498
+
499
+ def _setup_logging(self):
500
+ """Set up the logging directory and initialize log files."""
501
+ # Create logs directory if it doesn't exist
502
+ base_logs_dir = "logs"
503
+ os.makedirs(base_logs_dir, exist_ok=True)
504
+
505
+ # Create timestamped subdirectory for this session
506
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
507
+ self.session_logs_dir = os.path.join(base_logs_dir, timestamp, "display")
508
+ os.makedirs(self.session_logs_dir, exist_ok=True)
509
+
510
+ # Initialize log file paths with simple names
511
+ self.agent_log_files = {}
512
+ self.system_log_file = os.path.join(self.session_logs_dir, "system.txt")
513
+
514
+ # Initialize system log file
515
+ with open(self.system_log_file, "w", encoding="utf-8") as f:
516
+ f.write(f"MassGen System Messages Log\n")
517
+ f.write(
518
+ f"Session started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
519
+ )
520
+ f.write("=" * 80 + "\n\n")
521
+
522
+ def _get_agent_log_file(self, agent_id: int) -> str:
523
+ """Get or create the log file path for a specific agent."""
524
+ if agent_id not in self.agent_log_files:
525
+ # Use simple filename: agent_0.txt, agent_1.txt, etc.
526
+ self.agent_log_files[agent_id] = os.path.join(
527
+ self.session_logs_dir, f"agent_{agent_id}.txt"
528
+ )
529
+
530
+ # Initialize agent log file
531
+ with open(self.agent_log_files[agent_id], "w", encoding="utf-8") as f:
532
+ f.write(f"MassGen Agent {agent_id} Output Log\n")
533
+ f.write(
534
+ f"Session started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
535
+ )
536
+ f.write("=" * 80 + "\n\n")
537
+
538
+ return self.agent_log_files[agent_id]
539
+
540
+ def get_agent_log_path_for_display(self, agent_id: int) -> str:
541
+ """Get the log file path for display purposes (clickable link)."""
542
+ if not self.save_logs:
543
+ return ""
544
+
545
+ # Ensure the log file exists by calling _get_agent_log_file
546
+ log_path = self._get_agent_log_file(agent_id)
547
+
548
+ # Return relative path for better display
549
+ return log_path
550
+
551
+ def get_agent_answer_path_for_display(self, agent_id: int) -> str:
552
+ """Get the answer file path for display purposes (clickable link)."""
553
+ if not self.save_logs or not self.answers_dir:
554
+ return ""
555
+
556
+ # Construct answer file path using the answers directory
557
+ answer_file_path = os.path.join(self.answers_dir, f"agent_{agent_id}.txt")
558
+
559
+ # Return relative path for better display
560
+ return answer_file_path
561
+
562
+ def get_system_log_path_for_display(self) -> str:
563
+ """Get the system log file path for display purposes (clickable link)."""
564
+ if not self.save_logs:
565
+ return ""
566
+
567
+ return self.system_log_file
568
+
569
+ def _write_agent_log(self, agent_id: int, content: str):
570
+ """Write content to the agent's log file."""
571
+ if not self.save_logs:
572
+ return
573
+
574
+ try:
575
+ log_file = self._get_agent_log_file(agent_id)
576
+ with open(log_file, "a", encoding="utf-8") as f:
577
+ f.write(content)
578
+ f.flush() # Ensure immediate write
579
+ except Exception as e:
580
+ print(f"Error writing to agent {agent_id} log: {e}")
581
+
582
+ def _write_system_log(self, message: str):
583
+ """Write a system message to the system log file."""
584
+ if not self.save_logs:
585
+ return
586
+
587
+ try:
588
+ with open(self.system_log_file, "a", encoding="utf-8") as f:
589
+ timestamp = datetime.now().strftime("%H:%M:%S")
590
+ f.write(f"[{timestamp}] {message}\n")
591
+ f.flush() # Ensure immediate write
592
+ except Exception as e:
593
+ print(f"Error writing to system log: {e}")
594
+
595
+ def stream_output_sync(self, agent_id: int, content: str):
596
+ """FIXED: Buffered streaming with debounced display updates."""
597
+ if not self.display_enabled:
598
+ return
599
+
600
+ with self._lock:
601
+ if agent_id not in self.agent_outputs:
602
+ self.agent_outputs[agent_id] = ""
603
+
604
+ # Handle special content markers for display vs logging
605
+ display_content = content
606
+ log_content = content
607
+
608
+ # Check for special markers (keep text markers for backward compatibility)
609
+ if content.startswith("[CODE_DISPLAY_ONLY]"):
610
+ # This content should only be shown in display, not logged
611
+ display_content = content[len("[CODE_DISPLAY_ONLY]") :]
612
+ log_content = "" # Don't log this content
613
+ elif content.startswith("[CODE_LOG_ONLY]"):
614
+ # This content should only be logged, not displayed
615
+ display_content = "" # Don't display this content
616
+ log_content = content[len("[CODE_LOG_ONLY]") :]
617
+
618
+ # Add to display output only if there's display content
619
+ if display_content:
620
+ self.agent_outputs[agent_id] += display_content
621
+
622
+ # Write to log file only if there's log content
623
+ if log_content:
624
+ self._write_agent_log(agent_id, log_content)
625
+
626
+ # CRITICAL FIX: Use debounced updates instead of immediate updates
627
+ if display_content:
628
+ self._schedule_display_update()
629
+
630
+ def _handle_terminal_resize(self):
631
+ """Handle terminal resize by resetting cached dimensions."""
632
+ try:
633
+ current_width = os.get_terminal_size().columns
634
+ if (
635
+ self._display_cache
636
+ and abs(current_width - self._display_cache["terminal_width"]) > 2
637
+ ):
638
+ # Even small changes should invalidate cache for border alignment
639
+ self._invalidate_display_cache()
640
+ return True
641
+ except:
642
+ # If we can't detect terminal size, invalidate cache to be safe
643
+ self._invalidate_display_cache()
644
+ return True
645
+ return False
646
+
647
+ def add_system_message(self, message: str):
648
+ """Add a system message with timestamp."""
649
+ with self._lock:
650
+ timestamp = datetime.now().strftime("%H:%M:%S")
651
+ formatted_message = f"[{timestamp}] {message}"
652
+ self.system_messages.append(formatted_message)
653
+
654
+ # Keep only recent messages
655
+ if len(self.system_messages) > 20:
656
+ self.system_messages = self.system_messages[-20:]
657
+
658
+ # Write to system log
659
+ self._write_system_log(formatted_message + "\n")
660
+
661
+ def format_agent_notification(
662
+ self, agent_id: int, notification_type: str, content: str
663
+ ):
664
+ """Format agent notifications for display."""
665
+ notification_emoji = {
666
+ "update": "📢",
667
+ "debate": "🗣️",
668
+ "presentation": "🎯",
669
+ "prompt": "💡",
670
+ }
671
+
672
+ emoji = notification_emoji.get(notification_type, "📨")
673
+ notification_msg = (
674
+ f"{emoji} Agent {agent_id} received {notification_type} notification"
675
+ )
676
+ self.add_system_message(notification_msg)
677
+
678
+ def _update_display_immediate(self):
679
+ """Immediate display update - called by the debounced scheduler."""
680
+ if not self.display_enabled:
681
+ return
682
+
683
+ try:
684
+ # Handle potential terminal resize
685
+ self._handle_terminal_resize()
686
+
687
+ # Use atomic terminal clearing
688
+ self._clear_terminal_atomic()
689
+
690
+ # Get sorted agent IDs for consistent ordering
691
+ agent_ids = sorted(self.agent_outputs.keys())
692
+ if not agent_ids:
693
+ return
694
+
695
+ # Get terminal dimensions and calculate display dimensions
696
+ num_agents = len(agent_ids)
697
+ col_width, total_width, terminal_width = self._calculate_layout(num_agents)
698
+ except Exception as e:
699
+ # Fallback to simple text output if display fails
700
+ print(f"Display error: {e}")
701
+ for agent_id in sorted(self.agent_outputs.keys()):
702
+ print(
703
+ f"Agent {agent_id}: {self.agent_outputs[agent_id][-100:]}"
704
+ ) # Last 100 chars
705
+ return
706
+
707
+ # Split content into lines for each agent and limit to max_lines
708
+ agent_lines = {}
709
+ max_lines = 0
710
+ for agent_id in agent_ids:
711
+ lines = self.agent_outputs[agent_id].split("\n")
712
+ # Keep only the last max_lines lines (tail behavior)
713
+ if len(lines) > self.max_lines:
714
+ lines = lines[-self.max_lines :]
715
+ agent_lines[agent_id] = lines
716
+ max_lines = max(max_lines, len(lines))
717
+
718
+ # Create horizontal border line - use the locked width
719
+ border_line = "─" * total_width
720
+
721
+ # Enhanced MassGen system header with fixed width
722
+ print("")
723
+
724
+ # ANSI color codes
725
+ BRIGHT_CYAN = "\033[96m"
726
+ BRIGHT_BLUE = "\033[94m"
727
+ BRIGHT_GREEN = "\033[92m"
728
+ BRIGHT_YELLOW = "\033[93m"
729
+ BRIGHT_MAGENTA = "\033[95m"
730
+ BRIGHT_RED = "\033[91m"
731
+ BRIGHT_WHITE = "\033[97m"
732
+ BOLD = "\033[1m"
733
+ RESET = "\033[0m"
734
+
735
+ # Header with exact width
736
+ header_top = f"{BRIGHT_CYAN}{BOLD}╔{'═' * (total_width - 2)}╗{RESET}"
737
+ print(header_top)
738
+
739
+ # Empty line
740
+ header_empty = f"{BRIGHT_CYAN}║{' ' * (total_width - 2)}║{RESET}"
741
+ print(header_empty)
742
+
743
+ # Title line with exact centering
744
+ title_text = "🚀 MassGen - Multi-Agent Scaling System 🚀"
745
+ title_line_content = self._pad_to_width(title_text, total_width - 2, "center")
746
+ title_line = f"{BRIGHT_CYAN}║{BRIGHT_YELLOW}{BOLD}{title_line_content}{RESET}{BRIGHT_CYAN}║{RESET}"
747
+ print(title_line)
748
+
749
+ # Subtitle line
750
+ subtitle_text = "🔬 Advanced Agent Collaboration Framework"
751
+ subtitle_line_content = self._pad_to_width(
752
+ subtitle_text, total_width - 2, "center"
753
+ )
754
+ subtitle_line = f"{BRIGHT_CYAN}║{BRIGHT_GREEN}{subtitle_line_content}{RESET}{BRIGHT_CYAN}║{RESET}"
755
+ print(subtitle_line)
756
+
757
+ # Empty line and bottom border
758
+ print(header_empty)
759
+ header_bottom = f"{BRIGHT_CYAN}{BOLD}╚{'═' * (total_width - 2)}╝{RESET}"
760
+ print(header_bottom)
761
+
762
+ # Agent section with perfect alignment
763
+ print(f"\n{border_line}")
764
+
765
+ # Agent headers with exact column widths
766
+ header_parts = []
767
+ for agent_id in agent_ids:
768
+ model_name = self.agent_models.get(agent_id, "")
769
+ status = self.agent_statuses.get(agent_id, "unknown")
770
+
771
+ # Status configuration
772
+ status_config = {
773
+ "working": {"emoji": "🔄", "color": BRIGHT_YELLOW},
774
+ "voted": {"emoji": "✅", "color": BRIGHT_GREEN},
775
+ "failed": {"emoji": "❌", "color": BRIGHT_RED},
776
+ "unknown": {"emoji": "❓", "color": BRIGHT_WHITE},
777
+ }
778
+
779
+ config = status_config.get(status, status_config["unknown"])
780
+ emoji = config["emoji"]
781
+ status_color = config["color"]
782
+
783
+ # Create agent header with exact width
784
+ if model_name:
785
+ agent_header = f"{emoji} {BRIGHT_CYAN}Agent {agent_id}{RESET} {BRIGHT_MAGENTA}({model_name}){RESET} {status_color}[{status}]{RESET}"
786
+ else:
787
+ agent_header = f"{emoji} {BRIGHT_CYAN}Agent {agent_id}{RESET} {status_color}[{status}]{RESET}"
788
+
789
+ header_content = self._pad_to_width(agent_header, col_width, "center")
790
+ # Validate width immediately
791
+ if self._get_display_width(header_content) != col_width:
792
+ # Fallback to simple text if formatting issues
793
+ simple_header = f"Agent {agent_id} [{status}]"
794
+ header_content = self._pad_to_width(simple_header, col_width, "center")
795
+ header_parts.append(header_content)
796
+
797
+ # Print agent header line with exact borders
798
+ try:
799
+ header_line = self._create_bordered_line(header_parts, total_width)
800
+ print(header_line)
801
+ except Exception as e:
802
+ # Fallback to simple border if formatting fails
803
+ print("─" * total_width)
804
+
805
+ # Agent state information line
806
+ state_parts = []
807
+ for agent_id in agent_ids:
808
+ chat_round = getattr(self, "_agent_chat_rounds", {}).get(agent_id, 0)
809
+ vote_target = getattr(self, "_agent_vote_targets", {}).get(agent_id)
810
+ update_count = getattr(self, "_agent_update_counts", {}).get(agent_id, 0)
811
+ votes_cast = getattr(self, "_agent_votes_cast", {}).get(agent_id, 0)
812
+
813
+ # Format state info with better handling of color codes (removed redundant status)
814
+ state_info = []
815
+ state_info.append(
816
+ f"{BRIGHT_WHITE}Round:{RESET} {BRIGHT_GREEN}{chat_round}{RESET}"
817
+ )
818
+ state_info.append(
819
+ f"{BRIGHT_WHITE}#Updates:{RESET} {BRIGHT_MAGENTA}{update_count}{RESET}"
820
+ )
821
+ state_info.append(
822
+ f"{BRIGHT_WHITE}#Votes:{RESET} {BRIGHT_CYAN}{votes_cast}{RESET}"
823
+ )
824
+ if vote_target:
825
+ state_info.append(
826
+ f"{BRIGHT_WHITE}Vote →{RESET} {BRIGHT_GREEN}{vote_target}{RESET}"
827
+ )
828
+ else:
829
+ state_info.append(f"{BRIGHT_WHITE}Vote →{RESET} None")
830
+
831
+ state_text = f"📊 {' | '.join(state_info)}"
832
+ # Ensure exact column width with improved padding
833
+ state_content = self._pad_to_width(state_text, col_width, "center")
834
+ state_parts.append(state_content)
835
+
836
+ # Validate state line consistency before printing
837
+ try:
838
+ state_line = self._create_bordered_line(state_parts, total_width)
839
+ print(state_line)
840
+ except Exception as e:
841
+ # Fallback to simple border if formatting fails
842
+ print("─" * total_width)
843
+
844
+ # Answer file information
845
+ if self.save_logs and (hasattr(self, "session_logs_dir") or self.answers_dir):
846
+ UNDERLINE = "\033[4m"
847
+ link_parts = []
848
+ for agent_id in agent_ids:
849
+ # Try to get answer file path first, fallback to log file path
850
+ answer_path = self.get_agent_answer_path_for_display(agent_id)
851
+ if answer_path:
852
+ # Shortened display path
853
+ display_path = (
854
+ answer_path.replace(os.getcwd() + "/", "")
855
+ if answer_path.startswith(os.getcwd())
856
+ else answer_path
857
+ )
858
+
859
+ # Safe path truncation with better width handling
860
+ prefix = "📄 Answers: "
861
+ # More conservative calculation
862
+ max_path_len = max(
863
+ 10, col_width - self._get_display_width(prefix) - 8
864
+ )
865
+ if len(display_path) > max_path_len:
866
+ display_path = "..." + display_path[-(max_path_len - 3) :]
867
+
868
+ link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
869
+ link_content = self._pad_to_width(link_text, col_width, "center")
870
+ else:
871
+ # Fallback to log file path if answer path not available
872
+ log_path = self.get_agent_log_path_for_display(agent_id)
873
+ if log_path:
874
+ display_path = (
875
+ log_path.replace(os.getcwd() + "/", "")
876
+ if log_path.startswith(os.getcwd())
877
+ else log_path
878
+ )
879
+ prefix = "📁 Log: "
880
+ max_path_len = max(
881
+ 10, col_width - self._get_display_width(prefix) - 8
882
+ )
883
+ if len(display_path) > max_path_len:
884
+ display_path = "..." + display_path[-(max_path_len - 3) :]
885
+ link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
886
+ link_content = self._pad_to_width(
887
+ link_text, col_width, "center"
888
+ )
889
+ else:
890
+ link_content = self._pad_to_width("", col_width, "center")
891
+ link_parts.append(link_content)
892
+
893
+ # Validate log line consistency
894
+ try:
895
+ log_line = self._create_bordered_line(link_parts, total_width)
896
+ print(log_line)
897
+ except Exception as e:
898
+ # Fallback to simple border if formatting fails
899
+ print("─" * total_width)
900
+
901
+ print(border_line)
902
+
903
+ # Content area with perfect column alignment - Apply validation to every content line
904
+ for line_idx in range(max_lines):
905
+ content_parts = []
906
+ for agent_id in agent_ids:
907
+ lines = agent_lines[agent_id]
908
+ content = lines[line_idx] if line_idx < len(lines) else ""
909
+
910
+ # Ensure exact column width for each content piece
911
+ padded_content = self._pad_to_width(content, col_width, "left")
912
+ content_parts.append(padded_content)
913
+
914
+ # Apply border validation to every content line for consistency
915
+ try:
916
+ content_line = self._create_bordered_line(content_parts, total_width)
917
+ print(content_line)
918
+ except Exception as e:
919
+ # Fallback: print content without borders to maintain functionality
920
+ simple_line = " | ".join(content_parts)[: total_width - 4] + " " * max(
921
+ 0, total_width - 4 - len(simple_line)
922
+ )
923
+ print(f"│ {simple_line} │")
924
+
925
+ # System status section with exact width
926
+ if self.system_messages or self.current_phase or self.vote_distribution:
927
+ print(f"\n{border_line}")
928
+
929
+ # System state header
930
+ phase_color = (
931
+ BRIGHT_YELLOW if self.current_phase == "collaboration" else BRIGHT_GREEN
932
+ )
933
+ consensus_color = BRIGHT_GREEN if self.consensus_reached else BRIGHT_RED
934
+ consensus_text = "✅ YES" if self.consensus_reached else "❌ NO"
935
+
936
+ system_state_info = []
937
+ system_state_info.append(
938
+ f"{BRIGHT_WHITE}Phase:{RESET} {phase_color}{self.current_phase.upper()}{RESET}"
939
+ )
940
+ system_state_info.append(
941
+ f"{BRIGHT_WHITE}Consensus:{RESET} {consensus_color}{consensus_text}{RESET}"
942
+ )
943
+ system_state_info.append(
944
+ f"{BRIGHT_WHITE}Debate Rounds:{RESET} {BRIGHT_CYAN}{self.debate_rounds}{RESET}"
945
+ )
946
+ if self.representative_agent_id:
947
+ system_state_info.append(
948
+ f"{BRIGHT_WHITE}Representative Agent:{RESET} {BRIGHT_GREEN}{self.representative_agent_id}{RESET}"
949
+ )
950
+ else:
951
+ system_state_info.append(
952
+ f"{BRIGHT_WHITE}Representative Agent:{RESET} None"
953
+ )
954
+
955
+ system_header_text = (
956
+ f"{BRIGHT_CYAN}📋 SYSTEM STATE{RESET} - {' | '.join(system_state_info)}"
957
+ )
958
+ system_header_line = self._create_system_bordered_line(
959
+ system_header_text, total_width
960
+ )
961
+ print(system_header_line)
962
+
963
+ # System log file link
964
+ if self.save_logs and hasattr(self, "system_log_file"):
965
+ system_log_path = self.get_system_log_path_for_display()
966
+ if system_log_path:
967
+ UNDERLINE = "\033[4m"
968
+ display_path = (
969
+ system_log_path.replace(os.getcwd() + "/", "")
970
+ if system_log_path.startswith(os.getcwd())
971
+ else system_log_path
972
+ )
973
+
974
+ # Safe path truncation with consistent width handling
975
+ prefix = "📁 Log: "
976
+ max_path_len = max(
977
+ 10, total_width - self._get_display_width(prefix) - 15
978
+ )
979
+ if len(display_path) > max_path_len:
980
+ display_path = "..." + display_path[-(max_path_len - 3) :]
981
+
982
+ system_link_text = f"{prefix}{UNDERLINE}{display_path}{RESET}"
983
+ system_link_line = self._create_system_bordered_line(
984
+ system_link_text, total_width
985
+ )
986
+ print(system_link_line)
987
+
988
+ print(border_line)
989
+
990
+ # System messages with exact width and validation
991
+ if self.consensus_reached and self.representative_agent_id is not None:
992
+ consensus_msg = f"🎉 CONSENSUS REACHED! Representative: Agent {self.representative_agent_id}"
993
+ consensus_line = self._create_system_bordered_line(
994
+ consensus_msg, total_width
995
+ )
996
+ print(consensus_line)
997
+
998
+ # Vote distribution with validation
999
+ if self.vote_distribution:
1000
+ vote_msg = "📊 Vote Distribution: " + ", ".join(
1001
+ [f"Agent {k}→{v} votes" for k, v in self.vote_distribution.items()]
1002
+ )
1003
+
1004
+ # Use the new safe wrapping method
1005
+ max_content_width = total_width - 2
1006
+ if self._get_display_width(vote_msg) <= max_content_width:
1007
+ vote_line = self._create_system_bordered_line(vote_msg, total_width)
1008
+ print(vote_line)
1009
+ else:
1010
+ # Wrap vote distribution using safe method
1011
+ vote_header = "📊 Vote Distribution:"
1012
+ header_line = self._create_system_bordered_line(
1013
+ vote_header, total_width
1014
+ )
1015
+ print(header_line)
1016
+
1017
+ for agent_id, votes in self.vote_distribution.items():
1018
+ vote_detail = f" Agent {agent_id}: {votes} votes"
1019
+ detail_line = self._create_system_bordered_line(
1020
+ vote_detail, total_width
1021
+ )
1022
+ print(detail_line)
1023
+
1024
+ # Regular system messages with validation
1025
+ for message in self.system_messages:
1026
+ # Use consistent width calculation throughout
1027
+ max_content_width = total_width - 2
1028
+ if self._get_display_width(message) <= max_content_width:
1029
+ line = self._create_system_bordered_line(message, total_width)
1030
+ print(line)
1031
+ else:
1032
+ # Simple word wrapping
1033
+ words = message.split()
1034
+ current_line = ""
1035
+
1036
+ for word in words:
1037
+ test_line = f"{current_line} {word}".strip()
1038
+ if self._get_display_width(test_line) > max_content_width:
1039
+ # Print current line if it has content
1040
+ if current_line.strip():
1041
+ line = self._create_system_bordered_line(
1042
+ current_line.strip(), total_width
1043
+ )
1044
+ print(line)
1045
+ current_line = word
1046
+ else:
1047
+ current_line = test_line
1048
+
1049
+ # Print final line if it has content
1050
+ if current_line.strip():
1051
+ line = self._create_system_bordered_line(
1052
+ current_line.strip(), total_width
1053
+ )
1054
+ print(line)
1055
+
1056
+ # Final border
1057
+ print(border_line)
1058
+
1059
+ # Force output to be written immediately
1060
+ sys.stdout.flush()
1061
+
1062
+ def force_update_display(self):
1063
+ """Force an immediate display update (for status changes)."""
1064
+ with self._lock:
1065
+ if self._update_timer:
1066
+ self._update_timer.cancel()
1067
+ self._pending_update = True
1068
+ self._execute_display_update()
1069
+
1070
+
1071
+ class StreamingOrchestrator:
1072
+ def __init__(
1073
+ self,
1074
+ display_enabled: bool = True,
1075
+ stream_callback: Optional[Callable] = None,
1076
+ max_lines: int = 10,
1077
+ save_logs: bool = True,
1078
+ answers_dir: Optional[str] = None,
1079
+ ):
1080
+ self.display = MultiRegionDisplay(
1081
+ display_enabled, max_lines, save_logs, answers_dir
1082
+ )
1083
+ self.stream_callback = stream_callback
1084
+
1085
+ def stream_output(self, agent_id: int, content: str):
1086
+ """Streaming content - uses debounced updates."""
1087
+ self.display.stream_output_sync(agent_id, content)
1088
+ if self.stream_callback:
1089
+ try:
1090
+ self.stream_callback(agent_id, content)
1091
+ except Exception:
1092
+ pass
1093
+
1094
+ def set_agent_model(self, agent_id: int, model_name: str):
1095
+ """Set agent model - immediate update."""
1096
+ self.display.set_agent_model(agent_id, model_name)
1097
+ self.display.force_update_display()
1098
+
1099
+ def update_agent_status(self, agent_id: int, status: str):
1100
+ """Update agent status - immediate update for critical state changes."""
1101
+ self.display.update_agent_status(agent_id, status)
1102
+ self.display.force_update_display()
1103
+
1104
+ def update_phase(self, old_phase: str, new_phase: str):
1105
+ """Update phase - immediate update for critical state changes."""
1106
+ self.display.update_phase(old_phase, new_phase)
1107
+ self.display.force_update_display()
1108
+
1109
+ def update_vote_distribution(self, vote_dist: Dict[int, int]):
1110
+ """Update vote distribution - immediate update for critical state changes."""
1111
+ self.display.update_vote_distribution(vote_dist)
1112
+ self.display.force_update_display()
1113
+
1114
+ def update_consensus_status(
1115
+ self, representative_id: int, vote_dist: Dict[int, int]
1116
+ ):
1117
+ """Update consensus status - immediate update for critical state changes."""
1118
+ self.display.update_consensus_status(representative_id, vote_dist)
1119
+ self.display.force_update_display()
1120
+
1121
+ def reset_consensus(self):
1122
+ """Reset consensus - immediate update for critical state changes."""
1123
+ self.display.reset_consensus()
1124
+ self.display.force_update_display()
1125
+
1126
+ def add_system_message(self, message: str):
1127
+ """Add system message - immediate update for important messages."""
1128
+ self.display.add_system_message(message)
1129
+ self.display.force_update_display()
1130
+
1131
+ def update_agent_vote_target(self, agent_id: int, target_id: Optional[int]):
1132
+ """Update agent vote target - immediate update for critical state changes."""
1133
+ self.display.update_agent_vote_target(agent_id, target_id)
1134
+ self.display.force_update_display()
1135
+
1136
+ def update_agent_chat_round(self, agent_id: int, round_num: int):
1137
+ """Update agent chat round - debounced update."""
1138
+ self.display.update_agent_chat_round(agent_id, round_num)
1139
+ # Don't force immediate update for chat rounds
1140
+
1141
+ def update_agent_update_count(self, agent_id: int, count: int):
1142
+ """Update agent update count - debounced update."""
1143
+ self.display.update_agent_update_count(agent_id, count)
1144
+ # Don't force immediate update for update counts
1145
+
1146
+ def update_agent_votes_cast(self, agent_id: int, votes_cast: int):
1147
+ """Update agent votes cast - immediate update for vote-related changes."""
1148
+ self.display.update_agent_votes_cast(agent_id, votes_cast)
1149
+ self.display.force_update_display()
1150
+
1151
+ def update_debate_rounds(self, rounds: int):
1152
+ """Update debate rounds - immediate update for critical state changes."""
1153
+ self.display.update_debate_rounds(rounds)
1154
+ self.display.force_update_display()
1155
+
1156
+ def format_agent_notification(
1157
+ self, agent_id: int, notification_type: str, content: str
1158
+ ):
1159
+ """Format agent notifications - immediate update for notifications."""
1160
+ self.display.format_agent_notification(agent_id, notification_type, content)
1161
+ self.display.force_update_display()
1162
+
1163
+ def get_agent_log_path(self, agent_id: int) -> str:
1164
+ """Get the log file path for a specific agent."""
1165
+ return self.display.get_agent_log_path_for_display(agent_id)
1166
+
1167
+ def get_agent_answer_path(self, agent_id: int) -> str:
1168
+ """Get the answer file path for a specific agent."""
1169
+ return self.display.get_agent_answer_path_for_display(agent_id)
1170
+
1171
+ def get_system_log_path(self) -> str:
1172
+ """Get the system log file path."""
1173
+ return self.display.get_system_log_path_for_display()
1174
+
1175
+ def cleanup(self):
1176
+ """Clean up resources when orchestrator is no longer needed."""
1177
+ self.display.cleanup()
1178
+
1179
+
1180
+ def create_streaming_display(
1181
+ display_enabled: bool = True,
1182
+ stream_callback: Optional[Callable] = None,
1183
+ max_lines: int = 10,
1184
+ save_logs: bool = True,
1185
+ answers_dir: Optional[str] = None,
1186
+ ) -> StreamingOrchestrator:
1187
+ """Create a streaming orchestrator with display capabilities."""
1188
+ return StreamingOrchestrator(
1189
+ display_enabled, stream_callback, max_lines, save_logs, answers_dir
1190
+ )