massgen 0.0.3__py3-none-any.whl → 0.1.0a1__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 (268) hide show
  1. massgen/__init__.py +142 -8
  2. massgen/adapters/__init__.py +29 -0
  3. massgen/adapters/ag2_adapter.py +483 -0
  4. massgen/adapters/base.py +183 -0
  5. massgen/adapters/tests/__init__.py +0 -0
  6. massgen/adapters/tests/test_ag2_adapter.py +439 -0
  7. massgen/adapters/tests/test_agent_adapter.py +128 -0
  8. massgen/adapters/utils/__init__.py +2 -0
  9. massgen/adapters/utils/ag2_utils.py +236 -0
  10. massgen/adapters/utils/tests/__init__.py +0 -0
  11. massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
  12. massgen/agent_config.py +329 -55
  13. massgen/api_params_handler/__init__.py +10 -0
  14. massgen/api_params_handler/_api_params_handler_base.py +99 -0
  15. massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
  16. massgen/api_params_handler/_claude_api_params_handler.py +113 -0
  17. massgen/api_params_handler/_response_api_params_handler.py +130 -0
  18. massgen/backend/__init__.py +39 -4
  19. massgen/backend/azure_openai.py +385 -0
  20. massgen/backend/base.py +341 -69
  21. massgen/backend/base_with_mcp.py +1102 -0
  22. massgen/backend/capabilities.py +386 -0
  23. massgen/backend/chat_completions.py +577 -130
  24. massgen/backend/claude.py +1033 -537
  25. massgen/backend/claude_code.py +1203 -0
  26. massgen/backend/cli_base.py +209 -0
  27. massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
  28. massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
  29. massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
  30. massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
  31. massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
  32. massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
  33. massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
  34. massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
  35. massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
  36. massgen/backend/docs/inference_backend.md +257 -0
  37. massgen/backend/docs/permissions_and_context_files.md +1085 -0
  38. massgen/backend/external.py +126 -0
  39. massgen/backend/gemini.py +1850 -241
  40. massgen/backend/grok.py +40 -156
  41. massgen/backend/inference.py +156 -0
  42. massgen/backend/lmstudio.py +171 -0
  43. massgen/backend/response.py +1095 -322
  44. massgen/chat_agent.py +131 -113
  45. massgen/cli.py +1504 -287
  46. massgen/config_builder.py +2165 -0
  47. massgen/configs/BACKEND_CONFIGURATION.md +458 -0
  48. massgen/configs/README.md +559 -216
  49. massgen/configs/ag2/ag2_case_study.yaml +27 -0
  50. massgen/configs/ag2/ag2_coder.yaml +34 -0
  51. massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
  52. massgen/configs/ag2/ag2_gemini.yaml +27 -0
  53. massgen/configs/ag2/ag2_groupchat.yaml +108 -0
  54. massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
  55. massgen/configs/ag2/ag2_single_agent.yaml +21 -0
  56. massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
  57. massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
  58. massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
  59. massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
  60. massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
  61. massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
  62. massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
  63. massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
  64. massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
  65. massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
  66. massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
  67. massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
  68. massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
  69. massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
  70. massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
  71. massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
  72. massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
  73. massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
  74. massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
  75. massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
  76. massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
  77. massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
  78. massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
  79. massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
  80. massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
  81. massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
  82. massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
  83. massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
  84. massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
  85. massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
  86. massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
  87. massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
  88. massgen/configs/debug/skip_coordination_test.yaml +27 -0
  89. massgen/configs/debug/test_sdk_migration.yaml +17 -0
  90. massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
  91. massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
  92. massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
  93. massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
  94. massgen/configs/providers/claude/claude.yaml +14 -0
  95. massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
  96. massgen/configs/providers/local/lmstudio.yaml +11 -0
  97. massgen/configs/providers/openai/gpt5.yaml +46 -0
  98. massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
  99. massgen/configs/providers/others/grok_single_agent.yaml +19 -0
  100. massgen/configs/providers/others/zai_coding_team.yaml +108 -0
  101. massgen/configs/providers/others/zai_glm45.yaml +12 -0
  102. massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
  103. massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
  104. massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
  105. massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
  106. massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
  107. massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
  108. massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
  109. massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
  110. massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
  111. massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
  112. massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
  113. massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
  114. massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
  115. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
  116. massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
  117. massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
  118. massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
  119. massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
  120. massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
  121. massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
  122. massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
  123. massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
  124. massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
  125. massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
  126. massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
  127. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
  128. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
  129. massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
  130. massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
  131. massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
  132. massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
  133. massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
  134. massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
  135. massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
  136. massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
  137. massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
  138. massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
  139. massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
  140. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
  141. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
  142. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
  143. massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
  144. massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
  145. massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
  146. massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
  147. massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
  148. massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
  149. massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
  150. massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
  151. massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
  152. massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
  153. massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
  154. massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
  155. massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
  156. massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
  157. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
  158. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
  159. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
  160. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
  161. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
  162. massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
  163. massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
  164. massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
  165. massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
  166. massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
  167. massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
  168. massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
  169. massgen/coordination_tracker.py +708 -0
  170. massgen/docker/README.md +462 -0
  171. massgen/filesystem_manager/__init__.py +21 -0
  172. massgen/filesystem_manager/_base.py +9 -0
  173. massgen/filesystem_manager/_code_execution_server.py +545 -0
  174. massgen/filesystem_manager/_docker_manager.py +477 -0
  175. massgen/filesystem_manager/_file_operation_tracker.py +248 -0
  176. massgen/filesystem_manager/_filesystem_manager.py +813 -0
  177. massgen/filesystem_manager/_path_permission_manager.py +1261 -0
  178. massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
  179. massgen/formatter/__init__.py +10 -0
  180. massgen/formatter/_chat_completions_formatter.py +284 -0
  181. massgen/formatter/_claude_formatter.py +235 -0
  182. massgen/formatter/_formatter_base.py +156 -0
  183. massgen/formatter/_response_formatter.py +263 -0
  184. massgen/frontend/__init__.py +1 -2
  185. massgen/frontend/coordination_ui.py +471 -286
  186. massgen/frontend/displays/base_display.py +56 -11
  187. massgen/frontend/displays/create_coordination_table.py +1956 -0
  188. massgen/frontend/displays/rich_terminal_display.py +1259 -619
  189. massgen/frontend/displays/simple_display.py +9 -4
  190. massgen/frontend/displays/terminal_display.py +27 -68
  191. massgen/logger_config.py +681 -0
  192. massgen/mcp_tools/README.md +232 -0
  193. massgen/mcp_tools/__init__.py +105 -0
  194. massgen/mcp_tools/backend_utils.py +1035 -0
  195. massgen/mcp_tools/circuit_breaker.py +195 -0
  196. massgen/mcp_tools/client.py +894 -0
  197. massgen/mcp_tools/config_validator.py +138 -0
  198. massgen/mcp_tools/docs/circuit_breaker.md +646 -0
  199. massgen/mcp_tools/docs/client.md +950 -0
  200. massgen/mcp_tools/docs/config_validator.md +478 -0
  201. massgen/mcp_tools/docs/exceptions.md +1165 -0
  202. massgen/mcp_tools/docs/security.md +854 -0
  203. massgen/mcp_tools/exceptions.py +338 -0
  204. massgen/mcp_tools/hooks.py +212 -0
  205. massgen/mcp_tools/security.py +780 -0
  206. massgen/message_templates.py +342 -64
  207. massgen/orchestrator.py +1515 -241
  208. massgen/stream_chunk/__init__.py +35 -0
  209. massgen/stream_chunk/base.py +92 -0
  210. massgen/stream_chunk/multimodal.py +237 -0
  211. massgen/stream_chunk/text.py +162 -0
  212. massgen/tests/mcp_test_server.py +150 -0
  213. massgen/tests/multi_turn_conversation_design.md +0 -8
  214. massgen/tests/test_azure_openai_backend.py +156 -0
  215. massgen/tests/test_backend_capabilities.py +262 -0
  216. massgen/tests/test_backend_event_loop_all.py +179 -0
  217. massgen/tests/test_chat_completions_refactor.py +142 -0
  218. massgen/tests/test_claude_backend.py +15 -28
  219. massgen/tests/test_claude_code.py +268 -0
  220. massgen/tests/test_claude_code_context_sharing.py +233 -0
  221. massgen/tests/test_claude_code_orchestrator.py +175 -0
  222. massgen/tests/test_cli_backends.py +180 -0
  223. massgen/tests/test_code_execution.py +679 -0
  224. massgen/tests/test_external_agent_backend.py +134 -0
  225. massgen/tests/test_final_presentation_fallback.py +237 -0
  226. massgen/tests/test_gemini_planning_mode.py +351 -0
  227. massgen/tests/test_grok_backend.py +7 -10
  228. massgen/tests/test_http_mcp_server.py +42 -0
  229. massgen/tests/test_integration_simple.py +198 -0
  230. massgen/tests/test_mcp_blocking.py +125 -0
  231. massgen/tests/test_message_context_building.py +29 -47
  232. massgen/tests/test_orchestrator_final_presentation.py +48 -0
  233. massgen/tests/test_path_permission_manager.py +2087 -0
  234. massgen/tests/test_rich_terminal_display.py +14 -13
  235. massgen/tests/test_timeout.py +133 -0
  236. massgen/tests/test_v3_3agents.py +11 -12
  237. massgen/tests/test_v3_simple.py +8 -13
  238. massgen/tests/test_v3_three_agents.py +11 -18
  239. massgen/tests/test_v3_two_agents.py +8 -13
  240. massgen/token_manager/__init__.py +7 -0
  241. massgen/token_manager/token_manager.py +400 -0
  242. massgen/utils.py +52 -16
  243. massgen/v1/agent.py +45 -91
  244. massgen/v1/agents.py +18 -53
  245. massgen/v1/backends/gemini.py +50 -153
  246. massgen/v1/backends/grok.py +21 -54
  247. massgen/v1/backends/oai.py +39 -111
  248. massgen/v1/cli.py +36 -93
  249. massgen/v1/config.py +8 -12
  250. massgen/v1/logging.py +43 -127
  251. massgen/v1/main.py +18 -32
  252. massgen/v1/orchestrator.py +68 -209
  253. massgen/v1/streaming_display.py +62 -163
  254. massgen/v1/tools.py +8 -12
  255. massgen/v1/types.py +9 -23
  256. massgen/v1/utils.py +5 -23
  257. massgen-0.1.0a1.dist-info/METADATA +1287 -0
  258. massgen-0.1.0a1.dist-info/RECORD +273 -0
  259. massgen-0.1.0a1.dist-info/entry_points.txt +2 -0
  260. massgen/frontend/logging/__init__.py +0 -9
  261. massgen/frontend/logging/realtime_logger.py +0 -197
  262. massgen-0.0.3.dist-info/METADATA +0 -568
  263. massgen-0.0.3.dist-info/RECORD +0 -76
  264. massgen-0.0.3.dist-info/entry_points.txt +0 -2
  265. /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
  266. {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1956 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Multi-Agent Coordination Event Table Generator
5
+
6
+ Parses coordination_events.json and generates a formatted table showing
7
+ the progression of agent interactions across rounds.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Dict, List, Optional, Union
15
+
16
+ try:
17
+ from rich import box
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ RICH_AVAILABLE = True
22
+ except ImportError:
23
+ RICH_AVAILABLE = False
24
+
25
+
26
+ def display_scrollable_content_macos(
27
+ console: Console,
28
+ content_items: List[Any],
29
+ title: str = "",
30
+ ) -> None:
31
+ """
32
+ Display scrollable content with macOS-compatible navigation.
33
+ Works around macOS Terminal's issues with Rich's pager.
34
+ """
35
+ if not content_items:
36
+ console.print("[dim]No content to display[/dim]")
37
+ return
38
+
39
+ # Clear screen and move cursor to top
40
+ console.clear()
41
+
42
+ # Move cursor to top-left corner to ensure we start at the beginning
43
+ console.print("\033[H", end="")
44
+
45
+ # Print title if provided
46
+ if title:
47
+ console.print(f"\n[bold bright_green]{title}[/bold bright_green]\n")
48
+
49
+ # Print content
50
+ for item in content_items:
51
+ console.print(item)
52
+
53
+ # Show instructions and wait for input
54
+ console.print("\n" + "=" * 80)
55
+ console.print(
56
+ "[bright_cyan]Press Enter to return to agent selector...[/bright_cyan]",
57
+ )
58
+
59
+ try:
60
+ input() # Wait for Enter key
61
+ except (KeyboardInterrupt, EOFError):
62
+ pass # Handle Ctrl+C gracefully
63
+
64
+
65
+ def display_with_native_pager(
66
+ console: Console,
67
+ content_items: List[Any],
68
+ title: str = "",
69
+ ) -> None:
70
+ """
71
+ Use the system's native pager (less/more) for better scrolling support.
72
+ Falls back to simple display if pager is not available.
73
+ """
74
+ import subprocess
75
+ import tempfile
76
+
77
+ try:
78
+ # Create temporary file with content
79
+ with tempfile.NamedTemporaryFile(
80
+ mode="w",
81
+ suffix=".txt",
82
+ delete=False,
83
+ ) as tmp_file:
84
+ if title:
85
+ tmp_file.write(f"{title}\n")
86
+ tmp_file.write("=" * len(title) + "\n\n")
87
+
88
+ # Convert Rich content to plain text
89
+ for item in content_items:
90
+ if hasattr(item, "__rich_console__"):
91
+ # For Rich objects, render to plain text
92
+ with console.capture() as capture:
93
+ console.print(item)
94
+ tmp_file.write(capture.get() + "\n")
95
+ else:
96
+ tmp_file.write(str(item) + "\n")
97
+
98
+ tmp_file.write("\n" + "=" * 80 + "\n")
99
+ tmp_file.write("Press 'q' to quit, arrow keys or j/k to scroll\n")
100
+ tmp_file_path = tmp_file.name
101
+
102
+ # Use system pager
103
+ if sys.platform == "darwin": # macOS
104
+ pager_cmd = [
105
+ "less",
106
+ "-R",
107
+ "-S",
108
+ ] # -R for colors, -S for no wrap, start at top
109
+ else:
110
+ pager_cmd = ["less", "-R"]
111
+
112
+ try:
113
+ subprocess.run(pager_cmd + [tmp_file_path], check=True)
114
+ except (subprocess.CalledProcessError, FileNotFoundError):
115
+ # Fallback to 'more' if 'less' is not available
116
+ try:
117
+ subprocess.run(["more", tmp_file_path], check=True)
118
+ except (subprocess.CalledProcessError, FileNotFoundError):
119
+ # Final fallback to simple display
120
+ display_scrollable_content_macos(console, content_items, title)
121
+
122
+ # Clean up temporary file
123
+ try:
124
+ os.unlink(tmp_file_path)
125
+ except OSError:
126
+ pass
127
+
128
+ except Exception:
129
+ # Fallback to simple display on any error
130
+ display_scrollable_content_macos(console, content_items, title)
131
+
132
+
133
+ def is_macos_terminal() -> bool:
134
+ """Check if running in macOS Terminal or similar."""
135
+ if sys.platform != "darwin":
136
+ return False
137
+
138
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
139
+ return term_program in ["apple_terminal", "terminal", "iterm.app", ""]
140
+
141
+
142
+ def get_optimal_display_method() -> Any:
143
+ """Get the optimal display method for the current platform."""
144
+ if sys.platform == "darwin":
145
+ # Try native pager first on all macOS terminals since less works well
146
+ return "native_pager"
147
+ else:
148
+ return "rich_pager" # Use Rich's pager on Linux/Windows
149
+
150
+
151
+ @dataclass
152
+ class AgentState:
153
+ """Track state for a single agent"""
154
+
155
+ status: str = "idle"
156
+ current_answer: Optional[str] = None
157
+ answer_preview: Optional[str] = None
158
+ vote: Optional[str] = None
159
+ vote_reason: Optional[str] = None
160
+ context: List[str] = field(default_factory=list)
161
+ round: int = 0
162
+ is_final: bool = False
163
+ has_final_answer: bool = False
164
+ is_selected_winner: bool = False
165
+ has_voted: bool = False # Track if agent has already voted
166
+
167
+
168
+ @dataclass
169
+ class RoundData:
170
+ """Data for a single round"""
171
+
172
+ round_num: int
173
+ round_type: str # "R0", "R1", "R2", ... "FINAL"
174
+ agent_states: Dict[str, AgentState]
175
+
176
+
177
+ class CoordinationTableBuilder:
178
+ def __init__(self, data: Union[List[Dict[str, Any]], Dict[str, Any]]):
179
+ # Handle both old format (list of events) and new format (dict with
180
+ # metadata)
181
+ if isinstance(data, dict) and "events" in data:
182
+ self.events = data["events"]
183
+ self.session_metadata = data.get("session_metadata", {})
184
+ else:
185
+ self.events = data if isinstance(data, list) else []
186
+ self.session_metadata = {}
187
+
188
+ self.agents = self._extract_agents()
189
+ self.agent_mapping = self._create_agent_mapping()
190
+ self.agent_answers = self._extract_answer_previews()
191
+ self.final_winner = self._find_final_winner()
192
+ self.final_round_num = self._find_final_round_number()
193
+ self.agent_vote_rounds = self._track_vote_rounds()
194
+ self.rounds = self._process_events()
195
+ self.user_question = self._extract_user_question()
196
+
197
+ def _extract_agents(self) -> List[str]:
198
+ """Extract unique agent IDs from events using original orchestrator order"""
199
+ # First try to get agent order from session metadata
200
+ metadata_agents = self.session_metadata.get("agent_ids", [])
201
+ if metadata_agents:
202
+ return list(metadata_agents)
203
+
204
+ # Fallback: extract from events and sort for consistency
205
+ agents = set()
206
+ for event in self.events:
207
+ agent_id = event.get("agent_id")
208
+ if agent_id and agent_id not in [None, "null"]:
209
+ agents.add(agent_id)
210
+ return sorted(list(agents))
211
+
212
+ def _create_agent_mapping(self) -> Dict[str, str]:
213
+ """Create explicit mapping from agent_id to agent_number for answer labels"""
214
+ mapping = {}
215
+ for i, agent_id in enumerate(self.agents, 1):
216
+ mapping[agent_id] = str(i)
217
+ return mapping
218
+
219
+ def _extract_user_question(self) -> str:
220
+ """Extract the user question from session metadata"""
221
+ return str(
222
+ self.session_metadata.get("user_prompt", "No user prompt found"),
223
+ )
224
+
225
+ def _extract_answer_previews(self) -> Dict[str, str]:
226
+ """Extract the actual answer text for each agent using explicit mapping"""
227
+ answers = {}
228
+
229
+ # Try to get from final_agent_selected event
230
+ for event in self.events:
231
+ if event["event_type"] == "final_agent_selected":
232
+ context = event.get("context", {})
233
+ answers_for_context = context.get("answers_for_context", {})
234
+
235
+ # Map answers to agents using explicit agent mapping
236
+ for label, answer in answers_for_context.items():
237
+ # Direct match: label is an agent_id
238
+ if label in self.agents:
239
+ answers[label] = answer
240
+ else:
241
+ # Map answer label to agent using our explicit mapping
242
+ # For labels like "agent1.1", extract the number and
243
+ # find matching agent
244
+ if label.startswith("agent") and "." in label:
245
+ try:
246
+ # Extract agent number from label (e.g.,
247
+ # "agent1.1" -> "1")
248
+ agent_num = label.split(".")[0][5:] # Remove "agent" prefix
249
+ # Find agent with this number in our mapping
250
+ for (
251
+ agent_id,
252
+ mapped_num,
253
+ ) in self.agent_mapping.items():
254
+ if mapped_num == agent_num:
255
+ answers[agent_id] = answer
256
+ break
257
+ except (IndexError, ValueError):
258
+ continue
259
+
260
+ return answers
261
+
262
+ def _find_final_winner(self) -> Optional[str]:
263
+ """Find which agent was selected as the final winner"""
264
+ for event in self.events:
265
+ if event["event_type"] == "final_agent_selected":
266
+ agent_id = event.get("agent_id")
267
+ return agent_id if agent_id is not None else None
268
+ return None
269
+
270
+ def _find_final_round_number(self) -> Optional[int]:
271
+ """Find which round number is the final round"""
272
+ for event in self.events:
273
+ if event["event_type"] == "final_round_start":
274
+ context = event.get("context", {})
275
+ round_num = context.get("round", context.get("final_round"))
276
+ return int(round_num) if round_num is not None else None
277
+
278
+ # If no explicit final round, check for final_answer events
279
+ for event in self.events:
280
+ if event["event_type"] == "final_answer":
281
+ context = event.get("context", {})
282
+ round_num = context.get("round")
283
+ return int(round_num) if round_num is not None else None
284
+
285
+ return None
286
+
287
+ def _track_vote_rounds(self) -> Dict[str, int]:
288
+ """Track which round each agent cast their vote"""
289
+ vote_rounds = {}
290
+ for event in self.events:
291
+ if event["event_type"] == "vote_cast":
292
+ agent_id = event.get("agent_id")
293
+ context = event.get("context", {})
294
+ round_num = context.get("round", 0)
295
+ if agent_id:
296
+ vote_rounds[agent_id] = round_num
297
+ return vote_rounds
298
+
299
+ def _process_events(self) -> List[RoundData]:
300
+ """Process events into rounds with proper organization"""
301
+ # Find all unique rounds
302
+ all_rounds = set()
303
+ for event in self.events:
304
+ context = event.get("context", {})
305
+ round_num = context.get("round", 0)
306
+ all_rounds.add(round_num)
307
+
308
+ # Exclude final round from regular rounds if it exists
309
+ regular_rounds = sorted(
310
+ (all_rounds - {self.final_round_num} if self.final_round_num else all_rounds),
311
+ )
312
+
313
+ # Initialize round states
314
+ rounds = {}
315
+ for r in regular_rounds:
316
+ rounds[r] = {agent: AgentState(round=r) for agent in self.agents}
317
+
318
+ # Add final round if exists
319
+ if self.final_round_num is not None:
320
+ rounds[self.final_round_num] = {agent: AgentState(round=self.final_round_num) for agent in self.agents}
321
+
322
+ # Process events
323
+ for event in self.events:
324
+ event_type = event["event_type"]
325
+ agent_id = event.get("agent_id")
326
+ context = event.get("context", {})
327
+
328
+ if agent_id and agent_id in self.agents:
329
+ # Determine the round for this event
330
+ round_num = context.get("round", 0)
331
+
332
+ # Special handling for votes and answers that specify rounds
333
+ if event_type == "vote_cast":
334
+ round_num = context.get("round", 0)
335
+ elif event_type == "new_answer":
336
+ round_num = context.get("round", 0)
337
+ elif event_type == "restart_completed":
338
+ round_num = context.get(
339
+ "agent_round",
340
+ context.get("round", 0),
341
+ )
342
+ elif event_type == "final_answer":
343
+ round_num = self.final_round_num if self.final_round_num else context.get("round", 0)
344
+
345
+ if round_num in rounds:
346
+ agent_state = rounds[round_num][agent_id]
347
+
348
+ if event_type == "context_received":
349
+ labels = context.get("available_answer_labels", [])
350
+ agent_state.context = labels
351
+
352
+ elif event_type == "new_answer":
353
+ label = context.get("label")
354
+ if label:
355
+ agent_state.current_answer = label
356
+ # Get preview from saved answers
357
+ if agent_id in self.agent_answers:
358
+ agent_state.answer_preview = self.agent_answers[agent_id]
359
+
360
+ elif event_type == "vote_cast":
361
+ agent_state.vote = context.get("voted_for_label")
362
+ agent_state.vote_reason = context.get("reason")
363
+ agent_state.has_voted = True
364
+
365
+ elif event_type == "final_answer":
366
+ agent_state.has_final_answer = True
367
+ label = context.get("label")
368
+ agent_state.current_answer = f"Final answer provided ({label})"
369
+ agent_state.is_final = True
370
+ # Try to get the actual answer content if available
371
+ if agent_id in self.agent_answers:
372
+ agent_state.answer_preview = self.agent_answers[agent_id]
373
+ agent_state.current_answer = self.agent_answers[agent_id]
374
+
375
+ elif event_type == "final_agent_selected":
376
+ agent_state.is_selected_winner = True
377
+
378
+ elif event_type == "status_change":
379
+ status = event.get("details", "").replace(
380
+ "Changed to status: ",
381
+ "",
382
+ )
383
+ agent_state.status = status
384
+
385
+ # Mark non-winner as completed in FINAL round
386
+ if self.final_winner and self.final_round_num in rounds:
387
+ for agent in self.agents:
388
+ if agent != self.final_winner:
389
+ rounds[self.final_round_num][agent].status = "completed"
390
+
391
+ # Build final round list
392
+ round_list = []
393
+
394
+ # Add regular rounds
395
+ for r in regular_rounds:
396
+ round_type = f"R{r}"
397
+ round_list.append(
398
+ RoundData(
399
+ r,
400
+ round_type,
401
+ rounds.get(
402
+ r,
403
+ {agent: AgentState() for agent in self.agents},
404
+ ),
405
+ ),
406
+ )
407
+
408
+ # Add FINAL round if exists
409
+ if self.final_round_num is not None and self.final_round_num in rounds:
410
+ round_list.append(
411
+ RoundData(
412
+ self.final_round_num,
413
+ "FINAL",
414
+ rounds[self.final_round_num],
415
+ ),
416
+ )
417
+
418
+ return round_list
419
+
420
+ def _format_cell(self, content: str, width: int) -> str:
421
+ """Format content to fit within cell width, centered"""
422
+ if not content:
423
+ return " " * width
424
+
425
+ if len(content) <= width:
426
+ return content.center(width)
427
+ else:
428
+ # Truncate if too long
429
+ truncated = content[: width - 3] + "..."
430
+ return truncated.center(width)
431
+
432
+ def _build_agent_cell_content(
433
+ self,
434
+ agent_state: AgentState,
435
+ round_type: str,
436
+ agent_id: str,
437
+ round_num: int,
438
+ ) -> List[str]:
439
+ """Build the content for an agent's cell in a round"""
440
+ lines = []
441
+
442
+ # Determine if we should show context (but not for voting agents)
443
+ # Show context only if agent is doing something meaningful with it (but
444
+ # not voting)
445
+ show_context = (
446
+ (agent_state.current_answer and not agent_state.vote)
447
+ or agent_state.has_final_answer # Agent answered (but didn't vote)
448
+ or agent_state.status in ["streaming", "answering"] # Agent has final answer # Agent is actively working
449
+ )
450
+
451
+ # Don't show context for completed agents in FINAL round
452
+ if round_type == "FINAL" and agent_state.status == "completed":
453
+ show_context = False
454
+
455
+ # Add context if appropriate
456
+ if show_context:
457
+ if agent_state.context:
458
+ context_str = f"Context: [{', '.join(agent_state.context)}]"
459
+ else:
460
+ context_str = "Context: []"
461
+ lines.append(context_str)
462
+
463
+ # Add content based on what happened in this round
464
+ # Check for votes first, regardless of round type
465
+ if agent_state.vote:
466
+ # Agent voted in this round - show Context first, then vote
467
+ if agent_state.context:
468
+ lines.append(f"Context: [{', '.join(agent_state.context)}]")
469
+ lines.append(f"VOTE: {agent_state.vote}")
470
+ if agent_state.vote_reason:
471
+ reason = agent_state.vote_reason[:47] + "..." if len(agent_state.vote_reason) > 50 else agent_state.vote_reason
472
+ lines.append(f"Reason: {reason}")
473
+
474
+ elif round_type == "FINAL":
475
+ # Final presentation round
476
+ if agent_state.has_final_answer:
477
+ lines.append(f"FINAL ANSWER: {agent_state.current_answer}")
478
+ if agent_state.answer_preview:
479
+ clean_preview = agent_state.answer_preview.replace(
480
+ "\n",
481
+ " ",
482
+ ).strip()
483
+ lines.append(f"Preview: {clean_preview}")
484
+ else:
485
+ lines.append("Preview: [Answer not available]")
486
+ elif agent_state.status == "completed":
487
+ lines.append("(completed)")
488
+ else:
489
+ lines.append("(waiting)")
490
+
491
+ elif agent_state.current_answer and not agent_state.vote:
492
+ # Agent provided an answer in this round
493
+ lines.append(f"NEW ANSWER: {agent_state.current_answer}")
494
+ if agent_state.answer_preview:
495
+ clean_preview = agent_state.answer_preview.replace(
496
+ "\n",
497
+ " ",
498
+ ).strip()
499
+ lines.append(f"Preview: {clean_preview}")
500
+ else:
501
+ lines.append("Preview: [Answer not available]")
502
+
503
+ elif agent_state.status in ["streaming", "answering"]:
504
+ lines.append("(answering)")
505
+
506
+ elif agent_state.status == "voted":
507
+ lines.append("(voted)")
508
+
509
+ elif agent_state.status == "answered":
510
+ lines.append("(answered)")
511
+
512
+ else:
513
+ lines.append("(waiting)")
514
+
515
+ return lines
516
+
517
+ def generate_event_table(self) -> str:
518
+ """Generate an event-driven formatted table"""
519
+ num_agents = len(self.agents)
520
+ # Dynamic cell width based on number of agents
521
+ if num_agents <= 2:
522
+ cell_width = 60
523
+ elif num_agents == 3:
524
+ cell_width = 40
525
+ elif num_agents == 4:
526
+ cell_width = 30
527
+ else: # 5+ agents
528
+ cell_width = 25
529
+ total_width = 10 + (cell_width + 1) * num_agents + 1
530
+
531
+ lines = []
532
+
533
+ # Helper function to add separator
534
+ def add_separator(style: str = "-") -> None:
535
+ lines.append(
536
+ "|" + style * 10 + "+" + (style * cell_width + "+") * num_agents,
537
+ )
538
+
539
+ # Add legend/explanation section
540
+ lines.extend(self._create_legend_section(cell_width))
541
+
542
+ # Top border
543
+ lines.append("+" + "-" * (total_width - 2) + "+")
544
+
545
+ # Header row
546
+ header = "| Event |"
547
+ for agent in self.agents:
548
+ # Use format "Agent 1 (full_agent_id)"
549
+ agent_num = self.agent_mapping.get(agent, "?")
550
+ agent_name = f"Agent {agent_num} ({agent})"
551
+ header += self._format_cell(agent_name, cell_width) + "|"
552
+ lines.append(header)
553
+
554
+ # Header separator
555
+ lines.append(
556
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
557
+ )
558
+
559
+ # User question row
560
+ question_row = "| USER |"
561
+ question_width = cell_width * num_agents + (num_agents - 1)
562
+ question_text = self.user_question.center(question_width)
563
+ question_row += question_text + "|"
564
+ lines.append(question_row)
565
+
566
+ # Double separator
567
+ lines.append(
568
+ "|" + "=" * 10 + "+" + ("=" * cell_width + "+") * num_agents,
569
+ )
570
+
571
+ # Process events chronologically
572
+ agent_states: Dict[str, Dict[str, Any]] = {
573
+ agent: {
574
+ "status": "idle",
575
+ "context": [],
576
+ "answer": None,
577
+ "vote": None,
578
+ "preview": None,
579
+ "last_streaming_logged": False,
580
+ }
581
+ for agent in self.agents
582
+ }
583
+ event_num = 1
584
+
585
+ for event in self.events:
586
+ event_type = event["event_type"]
587
+ agent_id = event.get("agent_id")
588
+ context = event.get("context", {})
589
+
590
+ # Skip session-level events - just show the actual coordination
591
+ # work
592
+
593
+ # Skip iteration_start events - we already have session_start
594
+
595
+ # Skip system-level events without agent_id
596
+ if not agent_id or agent_id not in self.agents:
597
+ continue
598
+
599
+ # Update agent state and create table row
600
+
601
+ if event_type == "status_change":
602
+ status = event.get("details", "").replace(
603
+ "Changed to status: ",
604
+ "",
605
+ )
606
+ old_status = agent_states[agent_id]["status"]
607
+ agent_states[agent_id]["status"] = status
608
+
609
+ # Only log the FIRST streaming status for each agent, not
610
+ # repetitive ones
611
+ if status in ["streaming", "answering"]:
612
+ # Skip streaming that happens after voting - we'll show
613
+ # final_answer directly
614
+ if old_status == "voted":
615
+ # Just update status but don't show this event
616
+ pass
617
+ else:
618
+ # Only show if this is a meaningful transition (not
619
+ # streaming -> streaming)
620
+ if old_status not in ["streaming", "answering"] or not agent_states[agent_id]["last_streaming_logged"]:
621
+ # Create multi-line event with context and
622
+ # streaming start
623
+ event_lines = []
624
+ # Show context when starting to stream
625
+ context = agent_states[agent_id]["context"]
626
+ if context:
627
+ if isinstance(context, list):
628
+ context_str = ", ".join(str(c) for c in context)
629
+ else:
630
+ context_str = str(context)
631
+ event_lines.append(
632
+ f"📋 Context: [{context_str}]",
633
+ )
634
+ else:
635
+ event_lines.append("📋 Context: []")
636
+ event_lines.append(f"💭 Started {status}")
637
+
638
+ lines.extend(
639
+ self._create_multi_line_event_row(
640
+ event_num,
641
+ agent_id,
642
+ event_lines,
643
+ agent_states,
644
+ cell_width,
645
+ ),
646
+ )
647
+ add_separator("-") # Add separator after event
648
+ agent_states[agent_id]["last_streaming_logged"] = True
649
+ event_num += 1
650
+ elif status not in ["streaming", "answering"]:
651
+ # Reset the flag when status changes to something else
652
+ agent_states[agent_id]["last_streaming_logged"] = False
653
+
654
+ elif event_type == "context_received":
655
+ labels = context.get("available_answer_labels", [])
656
+ agent_states[agent_id]["context"] = labels
657
+ # Don't create a separate row for context, it will be shown
658
+ # with answers/votes
659
+
660
+ elif event_type == "restart_triggered":
661
+ # Show restart trigger event spanning both columns (it's a
662
+ # coordination event)
663
+ agent_num = self.agent_mapping.get(agent_id, "?")
664
+ agent_name = f"Agent {agent_num}"
665
+ lines.extend(
666
+ self._create_system_row(
667
+ f"🔁 {agent_name} RESTART TRIGGERED",
668
+ cell_width,
669
+ ),
670
+ )
671
+ event_num += 1
672
+
673
+ elif event_type == "restart_completed":
674
+ # Show restart completion
675
+ agent_round = context.get(
676
+ "agent_round",
677
+ context.get("round", 0),
678
+ )
679
+ lines.extend(
680
+ self._create_event_row(
681
+ event_num,
682
+ agent_id,
683
+ f"✅ RESTART COMPLETED (Restart {agent_round})",
684
+ agent_states,
685
+ cell_width,
686
+ ),
687
+ )
688
+ add_separator("-")
689
+ event_num += 1
690
+ # Reset streaming flag so next streaming will be shown
691
+ agent_states[agent_id]["last_streaming_logged"] = False
692
+
693
+ elif event_type == "new_answer":
694
+ label = context.get("label")
695
+ if label:
696
+ agent_states[agent_id]["answer"] = label
697
+ agent_states[agent_id]["status"] = "answered"
698
+ agent_states[agent_id]["last_streaming_logged"] = False # Reset for next round
699
+ # Get preview from saved answers
700
+ preview = ""
701
+ if agent_id in self.agent_answers:
702
+ preview = self.agent_answers[agent_id]
703
+ agent_states[agent_id]["preview"] = preview
704
+
705
+ # Create multi-line event with answer and preview
706
+ event_lines = []
707
+ # Context already shown when streaming started
708
+ event_lines.append(f"✨ NEW ANSWER: {label}")
709
+ if preview:
710
+ clean_preview = preview.replace("\n", " ").strip()
711
+ event_lines.append(f"👁️ Preview: {clean_preview}")
712
+
713
+ lines.extend(
714
+ self._create_multi_line_event_row(
715
+ event_num,
716
+ agent_id,
717
+ event_lines,
718
+ agent_states,
719
+ cell_width,
720
+ ),
721
+ )
722
+ add_separator("-") # Add separator after event
723
+ event_num += 1
724
+
725
+ elif event_type == "vote_cast":
726
+ vote = context.get("voted_for_label")
727
+ reason = context.get("reason", "")
728
+ if vote:
729
+ agent_states[agent_id]["vote"] = vote
730
+ agent_states[agent_id]["status"] = "voted"
731
+ agent_states[agent_id]["last_streaming_logged"] = False # Reset for next round
732
+
733
+ # Create multi-line event with vote and reason
734
+ event_lines = []
735
+ # Context already shown when streaming started
736
+ event_lines.append(f"🗳️ VOTE: {vote}")
737
+ if reason:
738
+ clean_reason = reason.replace("\n", " ").strip()
739
+ reason_str = clean_reason[:50] + "..." if len(clean_reason) > 50 else clean_reason
740
+ event_lines.append(f"💭 Reason: {reason_str}")
741
+
742
+ lines.extend(
743
+ self._create_multi_line_event_row(
744
+ event_num,
745
+ agent_id,
746
+ event_lines,
747
+ agent_states,
748
+ cell_width,
749
+ ),
750
+ )
751
+ add_separator("-") # Add separator after event
752
+ event_num += 1
753
+
754
+ elif event_type == "final_agent_selected":
755
+ # Show winner selection using agent mapping
756
+ agent_num = self.agent_mapping.get(agent_id, "?")
757
+ winner_name = f"Agent {agent_num}"
758
+ lines.extend(
759
+ self._create_system_row(
760
+ f"🏆 {winner_name} selected as winner",
761
+ cell_width,
762
+ ),
763
+ )
764
+ # Update other agents to completed status
765
+ for other_agent in self.agents:
766
+ if other_agent != agent_id:
767
+ agent_states[other_agent]["status"] = "completed"
768
+
769
+ elif event_type == "final_answer":
770
+ label = context.get("label")
771
+ if label:
772
+ agent_states[agent_id]["status"] = "final"
773
+
774
+ # Ensure preview is available for final answer
775
+ if not agent_states[agent_id]["preview"] and agent_id in self.agent_answers:
776
+ agent_states[agent_id]["preview"] = self.agent_answers[agent_id]
777
+
778
+ # Create multi-line event with final answer
779
+ event_lines = []
780
+ # Context already shown when streaming started
781
+ event_lines.append(f"🎯 FINAL ANSWER: {label}")
782
+ if agent_states[agent_id]["preview"]:
783
+ preview_text = str(agent_states[agent_id]["preview"])
784
+ clean_preview = preview_text.replace("\n", " ").strip()
785
+ event_lines.append(f"👁️ Preview: {clean_preview}")
786
+
787
+ lines.extend(
788
+ self._create_multi_line_event_row(
789
+ event_num,
790
+ agent_id,
791
+ event_lines,
792
+ agent_states,
793
+ cell_width,
794
+ ),
795
+ )
796
+ add_separator("-") # Add separator after event
797
+ event_num += 1
798
+
799
+ # Add summary statistics
800
+ lines.extend(self._create_summary_section(agent_states, cell_width))
801
+
802
+ # Bottom border
803
+ lines.append("+" + "-" * (total_width - 2) + "+")
804
+
805
+ return "\n".join(lines)
806
+
807
+ def _create_event_row(
808
+ self,
809
+ event_num: int,
810
+ active_agent: str,
811
+ event_description: str,
812
+ agent_states: dict,
813
+ cell_width: int,
814
+ ) -> list:
815
+ """Create a table row for a single event"""
816
+ row = "|"
817
+
818
+ # Event number
819
+ event_label = f" E{event_num} "
820
+ row += event_label[-10:].rjust(10) + "|"
821
+
822
+ # Agent cells
823
+ for agent in self.agents:
824
+ if agent == active_agent:
825
+ # This agent is performing the event
826
+ cell_content = event_description
827
+ else:
828
+ # Show current status for other agents - prioritize active
829
+ # states
830
+ status = agent_states[agent]["status"]
831
+ if status in ["streaming", "answering"]:
832
+ cell_content = f"🔄 ({status})"
833
+ elif status == "voted":
834
+ # Just show voted status without the value to avoid
835
+ # confusion
836
+ cell_content = "✅ (voted)"
837
+ elif status == "answered":
838
+ if agent_states[agent]["answer"]:
839
+ cell_content = f"✅ Answered: {agent_states[agent]['answer']}"
840
+ else:
841
+ cell_content = "✅ (answered)"
842
+ elif status == "completed":
843
+ cell_content = "✅ (completed)"
844
+ elif status == "final":
845
+ cell_content = "🎯 (final answer given)"
846
+ elif status == "idle":
847
+ cell_content = "⏳ (waiting)"
848
+ else:
849
+ cell_content = f"({status})"
850
+
851
+ row += self._format_cell(cell_content, cell_width) + "|"
852
+
853
+ return [row]
854
+
855
+ def _create_multi_line_event_row(
856
+ self,
857
+ event_num: int,
858
+ active_agent: str,
859
+ event_lines: list,
860
+ agent_states: dict,
861
+ cell_width: int,
862
+ ) -> list:
863
+ """Create multiple table rows for a single event with multiple lines of content"""
864
+ rows = []
865
+
866
+ for line_idx, event_line in enumerate(event_lines):
867
+ row = "|"
868
+
869
+ # Event number (only on first line)
870
+ if line_idx == 0:
871
+ event_label = f" E{event_num} "
872
+ row += event_label[-10:].rjust(10) + "|"
873
+ else:
874
+ row += " " * 10 + "|"
875
+
876
+ # Agent cells
877
+ for agent in self.agents:
878
+ if agent == active_agent:
879
+ # This agent is performing the event
880
+ cell_content = event_line
881
+ else:
882
+ # Show current status for other agents (only on first line)
883
+ # - prioritize active states
884
+ if line_idx == 0:
885
+ status = agent_states[agent]["status"]
886
+ if status in ["streaming", "answering"]:
887
+ cell_content = f"🔄 ({status})"
888
+ elif status == "voted":
889
+ # Just show voted status without the value to avoid
890
+ # confusion
891
+ cell_content = "✅ (voted)"
892
+ elif status == "answered":
893
+ if agent_states[agent]["answer"]:
894
+ cell_content = f"✅ Answered: {agent_states[agent]['answer']}"
895
+ else:
896
+ cell_content = "✅ (answered)"
897
+ elif status == "completed":
898
+ cell_content = "✅ (completed)"
899
+ elif status == "final":
900
+ cell_content = "🎯 (final answer given)"
901
+ elif status == "idle":
902
+ cell_content = "⏳ (waiting)"
903
+ else:
904
+ cell_content = f"({status})"
905
+ else:
906
+ cell_content = ""
907
+
908
+ row += self._format_cell(cell_content, cell_width) + "|"
909
+
910
+ rows.append(row)
911
+
912
+ return rows
913
+
914
+ def _create_system_row(self, message: str, cell_width: int) -> list:
915
+ """Create a system announcement row that spans all columns"""
916
+ total_width = 10 + (cell_width + 1) * len(self.agents) + 1
917
+
918
+ # Separator line
919
+ separator = "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents)
920
+
921
+ # Message row
922
+ message_width = total_width - 3 # Account for borders
923
+ message_row = "|" + message.center(message_width) + "|"
924
+
925
+ # Another separator
926
+ separator2 = "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents)
927
+
928
+ return [separator, message_row, separator2]
929
+
930
+ def _create_summary_section(
931
+ self,
932
+ agent_states: dict,
933
+ cell_width: int,
934
+ ) -> list:
935
+ """Create summary statistics section"""
936
+ lines = []
937
+
938
+ # Calculate statistics
939
+ total_answers = sum(1 for agent in self.agents if agent_states[agent]["answer"])
940
+ total_votes = sum(1 for agent in self.agents if agent_states[agent]["vote"])
941
+ total_restarts = len(
942
+ [e for e in self.events if e["event_type"] == "restart_completed"],
943
+ )
944
+
945
+ # Count per-agent stats
946
+ agent_stats = {}
947
+ for agent in self.agents:
948
+ agent_num = self.agent_mapping.get(agent, "?")
949
+ agent_name = f"Agent {agent_num}"
950
+ agent_stats[agent_name] = {
951
+ "answers": 1 if agent_states[agent]["answer"] else 0,
952
+ "votes": 1 if agent_states[agent]["vote"] else 0,
953
+ "final_status": agent_states[agent]["status"],
954
+ }
955
+
956
+ # Count restarts per agent
957
+ for event in self.events:
958
+ if event["event_type"] == "restart_completed" and event.get("agent_id") in self.agents:
959
+ agent_id = event["agent_id"]
960
+ agent_num = self.agent_mapping.get(agent_id, "?")
961
+ agent_name = f"Agent {agent_num}"
962
+ if agent_name not in agent_stats:
963
+ agent_stats[agent_name] = {"restarts": 0}
964
+ if "restarts" not in agent_stats[agent_name]:
965
+ agent_stats[agent_name]["restarts"] = 0
966
+ agent_stats[agent_name]["restarts"] += 1
967
+
968
+ # Create separator
969
+ separator = "|" + "=" * 10 + "+" + ("=" * cell_width + "+") * len(self.agents)
970
+ lines.append(separator)
971
+
972
+ # Summary header
973
+ summary_header = "| SUMMARY |"
974
+ for agent in self.agents:
975
+ agent_num = self.agent_mapping.get(agent, "?")
976
+ agent_name = f"Agent {agent_num}"
977
+ summary_header += self._format_cell(agent_name, cell_width) + "|"
978
+ lines.append(summary_header)
979
+
980
+ # Separator
981
+ lines.append(
982
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents),
983
+ )
984
+
985
+ # Answers row
986
+ answers_row = "| Answers |"
987
+ for agent in self.agents:
988
+ agent_num = self.agent_mapping.get(agent, "?")
989
+ agent_name = f"Agent {agent_num}"
990
+ count = agent_stats.get(agent_name, {}).get("answers", 0)
991
+ answers_row += (
992
+ self._format_cell(
993
+ f"{count} answer{'s' if count != 1 else ''}",
994
+ cell_width,
995
+ )
996
+ + "|"
997
+ )
998
+ lines.append(answers_row)
999
+
1000
+ # Votes row
1001
+ votes_row = "| Votes |"
1002
+ for agent in self.agents:
1003
+ agent_num = self.agent_mapping.get(agent, "?")
1004
+ agent_name = f"Agent {agent_num}"
1005
+ count = agent_stats.get(agent_name, {}).get("votes", 0)
1006
+ votes_row += (
1007
+ self._format_cell(
1008
+ f"{count} vote{'s' if count != 1 else ''}",
1009
+ cell_width,
1010
+ )
1011
+ + "|"
1012
+ )
1013
+ lines.append(votes_row)
1014
+
1015
+ # Restarts row
1016
+ restarts_row = "| Restarts |"
1017
+ for agent in self.agents:
1018
+ agent_num = self.agent_mapping.get(agent, "?")
1019
+ agent_name = f"Agent {agent_num}"
1020
+ count = agent_stats.get(agent_name, {}).get("restarts", 0)
1021
+ restarts_row += (
1022
+ self._format_cell(
1023
+ f"{count} restart{'s' if count != 1 else ''}",
1024
+ cell_width,
1025
+ )
1026
+ + "|"
1027
+ )
1028
+ lines.append(restarts_row)
1029
+
1030
+ # Final status row
1031
+ status_row = "| Status |"
1032
+ for agent in self.agents:
1033
+ agent_num = self.agent_mapping.get(agent, "?")
1034
+ agent_name = f"Agent {agent_num}"
1035
+ status = agent_states[agent]["status"]
1036
+ if status == "final":
1037
+ display = "🏆 Winner"
1038
+ elif status == "completed":
1039
+ display = "✅ Completed"
1040
+ elif status == "voted":
1041
+ display = "✅ Voted"
1042
+ else:
1043
+ display = f"({status})"
1044
+ status_row += self._format_cell(display, cell_width) + "|"
1045
+ lines.append(status_row)
1046
+
1047
+ # Overall totals row
1048
+ lines.append(
1049
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * len(self.agents),
1050
+ )
1051
+ totals_row = "| TOTALS |"
1052
+ total_width = cell_width * len(self.agents) + (len(self.agents) - 1)
1053
+ totals_content = f"{total_answers} answers, {total_votes} votes, {total_restarts} restarts"
1054
+ winner_name = None
1055
+ for agent in self.agents:
1056
+ if agent_states[agent]["status"] == "final":
1057
+ winner_name = f"Agent{agent.split('_')[-1]}" if "_" in agent else agent
1058
+ break
1059
+ if winner_name:
1060
+ totals_content += f" → {winner_name} selected"
1061
+ totals_row += totals_content.center(total_width) + "|"
1062
+ lines.append(totals_row)
1063
+
1064
+ return lines
1065
+
1066
+ def _get_legend_content(self) -> dict:
1067
+ """Get legend content as structured data to be formatted by different displays"""
1068
+ return {
1069
+ "event_symbols": [
1070
+ ("💭 Started streaming", "Agent begins thinking/processing"),
1071
+ ("✨ NEW ANSWER", "Agent provides a labeled answer"),
1072
+ ("🗳️ VOTE", "Agent votes for an answer"),
1073
+ ("💭 Reason", "Reasoning behind the vote"),
1074
+ ("👁️ Preview", "Content of the answer"),
1075
+ ("🔁 RESTART TRIGGERED", "Agent requests to restart"),
1076
+ ("✅ RESTART COMPLETED", "Agent finishes restart"),
1077
+ ("🎯 FINAL ANSWER", "Winner provides final response"),
1078
+ ("🏆 Winner selected", "System announces winner"),
1079
+ ],
1080
+ "status_symbols": [
1081
+ ("💭 (streaming)", "Currently thinking/processing"),
1082
+ ("⏳ (waiting)", "Idle, waiting for turn"),
1083
+ ("✅ (answered)", "Has provided an answer"),
1084
+ ("✅ (voted)", "Has cast a vote"),
1085
+ ("✅ (completed)", "Task completed"),
1086
+ ("🎯 (final answer given)", "Winner completed final answer"),
1087
+ ],
1088
+ "terms": [
1089
+ ("Context", "Available answer options agent can see"),
1090
+ ("Restart", "Agent starts over (clears memory)"),
1091
+ ("Event", "Chronological action in the coordination"),
1092
+ (
1093
+ "Answer Labels",
1094
+ "Each answer gets a unique ID (agent1.1, agent2.1, etc.)\n"
1095
+ " Format: agent{N}.{attempt} where N=agent number, attempt=new answer number\n"
1096
+ " Example: agent1.1 = Agent1's 1st answer, agent2.1 = Agent2's 1st answer",
1097
+ ),
1098
+ (
1099
+ "agent1.final",
1100
+ "Special label for the winner's final answer",
1101
+ ),
1102
+ ],
1103
+ }
1104
+
1105
+ def _create_legend_section(self, cell_width: int) -> list:
1106
+ """Create legend/explanation section at the top for plain text"""
1107
+ lines = []
1108
+ legend_data = self._get_legend_content()
1109
+
1110
+ # Title
1111
+ lines.append("")
1112
+ lines.append("Multi-Agent Coordination Events Log")
1113
+ lines.append("=" * 50)
1114
+ lines.append("")
1115
+
1116
+ # Event symbols
1117
+ lines.append("📋 EVENT SYMBOLS:")
1118
+ for symbol, description in legend_data["event_symbols"]:
1119
+ # Pad symbol to consistent width (24 chars) for alignment
1120
+ padded = f" {symbol}".ljust(28)
1121
+ lines.append(f"{padded}- {description}")
1122
+ lines.append("")
1123
+
1124
+ # Status symbols
1125
+ lines.append("📊 STATUS SYMBOLS:")
1126
+ for symbol, description in legend_data["status_symbols"]:
1127
+ padded = f" {symbol}".ljust(28)
1128
+ lines.append(f"{padded}- {description}")
1129
+ lines.append("")
1130
+
1131
+ # Terms
1132
+ lines.append("📖 TERMS:")
1133
+ for term, description in legend_data["terms"]:
1134
+ if "\n" in description:
1135
+ # Handle multi-line descriptions
1136
+ first_line = description.split("\n")[0]
1137
+ lines.append(f" {term.ljust(13)} - {first_line}")
1138
+ for line in description.split("\n")[1:]:
1139
+ lines.append(f" {line}")
1140
+ else:
1141
+ lines.append(f" {term.ljust(13)} - {description}")
1142
+ lines.append("")
1143
+
1144
+ return lines
1145
+
1146
+ def generate_table(self) -> str:
1147
+ """Generate the formatted table"""
1148
+ num_agents = len(self.agents)
1149
+ # Dynamic cell width based on number of agents
1150
+ if num_agents <= 2:
1151
+ cell_width = 60
1152
+ elif num_agents == 3:
1153
+ cell_width = 40
1154
+ elif num_agents == 4:
1155
+ cell_width = 30
1156
+ else: # 5+ agents
1157
+ cell_width = 25
1158
+ total_width = 10 + (cell_width + 1) * num_agents + 1
1159
+
1160
+ lines = []
1161
+
1162
+ # Top border
1163
+ lines.append("+" + "-" * (total_width - 2) + "+")
1164
+
1165
+ # Header row
1166
+ header = "| Round |"
1167
+ for agent in self.agents:
1168
+ # Try to create readable agent names
1169
+ # Use the full agent name as provided by user configuration
1170
+ agent_name = agent
1171
+ header += self._format_cell(agent_name, cell_width) + "|"
1172
+ lines.append(header)
1173
+
1174
+ # Header separator
1175
+ lines.append(
1176
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
1177
+ )
1178
+
1179
+ # User question row
1180
+ question_row = "| USER |"
1181
+ question_width = cell_width * num_agents + (num_agents - 1)
1182
+ question_text = self.user_question.center(question_width)
1183
+ question_row += question_text + "|"
1184
+ lines.append(question_row)
1185
+
1186
+ # Double separator
1187
+ lines.append(
1188
+ "|" + "=" * 10 + "+" + ("=" * cell_width + "+") * num_agents,
1189
+ )
1190
+
1191
+ # Process each round
1192
+ for i, round_data in enumerate(self.rounds):
1193
+ # Get content for each agent
1194
+ agent_contents = {}
1195
+ max_lines = 0
1196
+
1197
+ for agent in self.agents:
1198
+ content = self._build_agent_cell_content(
1199
+ round_data.agent_states[agent],
1200
+ round_data.round_type,
1201
+ agent,
1202
+ round_data.round_num,
1203
+ )
1204
+ agent_contents[agent] = content
1205
+ max_lines = max(max_lines, len(content))
1206
+
1207
+ # Build round rows
1208
+ for line_idx in range(max_lines):
1209
+ row = "|"
1210
+
1211
+ # Round label (only on first line)
1212
+ if line_idx == 0:
1213
+ if round_data.round_type == "FINAL":
1214
+ round_label = " FINAL "
1215
+ else:
1216
+ round_label = f" {round_data.round_type} "
1217
+ row += round_label[-10:].rjust(10) + "|"
1218
+ else:
1219
+ row += " " * 10 + "|"
1220
+
1221
+ # Agent cells
1222
+ for agent in self.agents:
1223
+ content_lines = agent_contents[agent]
1224
+ if line_idx < len(content_lines):
1225
+ row += self._format_cell(
1226
+ content_lines[line_idx],
1227
+ cell_width,
1228
+ )
1229
+ else:
1230
+ row += " " * cell_width
1231
+ row += "|"
1232
+
1233
+ lines.append(row)
1234
+
1235
+ # Round separator
1236
+ if i < len(self.rounds) - 1:
1237
+ next_round = self.rounds[i + 1]
1238
+ if next_round.round_type == "FINAL":
1239
+ # Add winner announcement before FINAL round
1240
+ lines.append(
1241
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
1242
+ )
1243
+
1244
+ # Winner announcement row
1245
+ if self.final_winner:
1246
+ # Use agent mapping for consistent naming
1247
+ agent_number = self.agent_mapping.get(
1248
+ self.final_winner,
1249
+ )
1250
+ if agent_number:
1251
+ winner_name = f"Agent {agent_number}"
1252
+ else:
1253
+ winner_name = self.final_winner
1254
+
1255
+ winner_text = f"{winner_name} selected as winner"
1256
+ winner_width = total_width - 1 # Full table width minus the outer borders
1257
+ winner_row = "|" + winner_text.center(winner_width) + "|"
1258
+ lines.append(winner_row)
1259
+
1260
+ # Solid line before FINAL
1261
+ lines.append(
1262
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
1263
+ )
1264
+ else:
1265
+ # Wavy line between regular rounds
1266
+ lines.append(
1267
+ "|" + "~" * 10 + "+" + ("~" * cell_width + "+") * num_agents,
1268
+ )
1269
+
1270
+ # Bottom separator
1271
+ lines.append(
1272
+ "|" + "-" * 10 + "+" + ("-" * cell_width + "+") * num_agents,
1273
+ )
1274
+
1275
+ # Bottom border
1276
+ lines.append("+" + "-" * (total_width - 2) + "+")
1277
+
1278
+ return "\n".join(lines)
1279
+
1280
+ def _create_rich_legend(self) -> Optional[Any]:
1281
+ """Create Rich legend panel using shared legend content"""
1282
+ try:
1283
+ from rich import box
1284
+ from rich.panel import Panel
1285
+ from rich.text import Text
1286
+ except ImportError:
1287
+ return None
1288
+
1289
+ legend_data = self._get_legend_content()
1290
+ content = Text()
1291
+
1292
+ # Event symbols
1293
+ content.append("📋 EVENT SYMBOLS:\n", style="bold bright_blue")
1294
+ for symbol, description in legend_data["event_symbols"]:
1295
+ padded = f" {symbol}".ljust(28)
1296
+ content.append(f"{padded}- {description}\n", style="dim white")
1297
+ content.append("\n")
1298
+
1299
+ # Status symbols
1300
+ content.append("📊 STATUS SYMBOLS:\n", style="bold bright_green")
1301
+ for symbol, description in legend_data["status_symbols"]:
1302
+ padded = f" {symbol}".ljust(28)
1303
+ content.append(f"{padded}- {description}\n", style="dim white")
1304
+ content.append("\n")
1305
+
1306
+ # Terms
1307
+ content.append("📖 TERMS:\n", style="bold bright_yellow")
1308
+ for term, description in legend_data["terms"]:
1309
+ if "\n" in description:
1310
+ # Handle multi-line descriptions
1311
+ lines = description.split("\n")
1312
+ content.append(
1313
+ f" {term.ljust(13)} - {lines[0]}\n",
1314
+ style="dim white",
1315
+ )
1316
+ for line in lines[1:]:
1317
+ content.append(f" {line}\n", style="dim white")
1318
+ else:
1319
+ content.append(
1320
+ f" {term.ljust(13)} - {description}\n",
1321
+ style="dim white",
1322
+ )
1323
+
1324
+ return Panel(
1325
+ content,
1326
+ title="[bold bright_cyan]📋 COORDINATION GUIDE[/bold bright_cyan]",
1327
+ border_style="bright_cyan",
1328
+ box=box.ROUNDED,
1329
+ padding=(1, 2),
1330
+ )
1331
+
1332
+ def generate_rich_event_table(self) -> Optional[tuple]:
1333
+ """Generate a rich event-driven table with legend
1334
+
1335
+ Returns:
1336
+ Tuple of (legend_panel, table) or None if Rich not available
1337
+ """
1338
+ try:
1339
+ from rich import box
1340
+ from rich.table import Table
1341
+ from rich.text import Text
1342
+ except ImportError:
1343
+ return None
1344
+
1345
+ # Create legend first
1346
+ legend = self._create_rich_legend()
1347
+
1348
+ # Create the main table
1349
+ table = Table(
1350
+ title="[bold cyan]Multi-Agent Coordination Events[/bold cyan]",
1351
+ box=box.DOUBLE_EDGE,
1352
+ expand=True,
1353
+ show_lines=True,
1354
+ )
1355
+
1356
+ # Add columns
1357
+ table.add_column(
1358
+ "Event",
1359
+ style="bold yellow",
1360
+ width=8,
1361
+ justify="center",
1362
+ )
1363
+ for agent in self.agents:
1364
+ # Use format "Agent 1 (full_agent_id)"
1365
+ agent_num = self.agent_mapping.get(agent, "?")
1366
+ agent_name = f"Agent {agent_num} ({agent})"
1367
+ table.add_column(
1368
+ agent_name,
1369
+ style="white",
1370
+ width=45,
1371
+ justify="center",
1372
+ )
1373
+
1374
+ # Add user question as header
1375
+ question_row = ["[bold cyan]USER[/bold cyan]"]
1376
+ question_text = f"[bold white]{self.user_question}[/bold white]"
1377
+ for _ in range(len(self.agents)):
1378
+ question_row.append(question_text)
1379
+ table.add_row(*question_row)
1380
+
1381
+ # Process events chronologically
1382
+ agent_states: Dict[str, Dict[str, Any]] = {
1383
+ agent: {
1384
+ "status": "idle",
1385
+ "context": [],
1386
+ "answer": None,
1387
+ "vote": None,
1388
+ "preview": None,
1389
+ "last_streaming_logged": False,
1390
+ }
1391
+ for agent in self.agents
1392
+ }
1393
+ event_num = 1
1394
+
1395
+ for event in self.events:
1396
+ event_type = event["event_type"]
1397
+ agent_id = event.get("agent_id")
1398
+ context = event.get("context", {})
1399
+
1400
+ # Handle system events that span both columns
1401
+ if event_type == "final_agent_selected":
1402
+ agent_num = self.agent_mapping.get(agent_id, "?")
1403
+ winner_name = f"Agent {agent_num}"
1404
+ winner_row = ["[bold green]🏆[/bold green]"]
1405
+ winner_text = Text(
1406
+ f"🏆 {winner_name} selected as winner 🏆",
1407
+ style="bold green",
1408
+ justify="center",
1409
+ )
1410
+ for _ in range(len(self.agents)):
1411
+ winner_row.append(winner_text)
1412
+ table.add_row(*winner_row)
1413
+ continue
1414
+ elif event_type == "restart_triggered" and agent_id and agent_id in self.agents:
1415
+ agent_num = self.agent_mapping.get(agent_id, "?")
1416
+ agent_name = f"Agent {agent_num}"
1417
+ restart_row = ["[bold yellow]🔁[/bold yellow]"]
1418
+ restart_text = Text(
1419
+ f"🔁 {agent_name} RESTART TRIGGERED",
1420
+ style="bold yellow",
1421
+ justify="center",
1422
+ )
1423
+ for _ in range(len(self.agents)):
1424
+ restart_row.append(restart_text)
1425
+ table.add_row(*restart_row)
1426
+ continue
1427
+
1428
+ # Skip session-level events
1429
+ if not agent_id or agent_id not in self.agents:
1430
+ continue
1431
+
1432
+ # Handle agent events
1433
+ if event_type == "status_change":
1434
+ status = event.get("details", "").replace(
1435
+ "Changed to status: ",
1436
+ "",
1437
+ )
1438
+ old_status = agent_states[agent_id]["status"]
1439
+ agent_states[agent_id]["status"] = status
1440
+
1441
+ # Only log first streaming
1442
+ if status in ["streaming", "answering"]:
1443
+ if old_status == "voted":
1444
+ pass # Skip post-vote streaming
1445
+ elif old_status not in ["streaming", "answering"] or not agent_states[agent_id]["last_streaming_logged"]:
1446
+ row = self._create_rich_event_row(
1447
+ event_num,
1448
+ agent_id,
1449
+ agent_states,
1450
+ "streaming_start",
1451
+ )
1452
+ if row:
1453
+ table.add_row(*row)
1454
+ event_num += 1
1455
+ agent_states[agent_id]["last_streaming_logged"] = True
1456
+
1457
+ elif event_type == "context_received":
1458
+ labels = context.get("available_answer_labels", [])
1459
+ agent_states[agent_id]["context"] = labels
1460
+
1461
+ elif event_type == "restart_completed":
1462
+ agent_round = context.get(
1463
+ "agent_round",
1464
+ context.get("round", 0),
1465
+ )
1466
+ row = self._create_rich_event_row(
1467
+ event_num,
1468
+ agent_id,
1469
+ agent_states,
1470
+ "restart_completed",
1471
+ agent_round,
1472
+ )
1473
+ if row:
1474
+ table.add_row(*row)
1475
+ event_num += 1
1476
+ agent_states[agent_id]["last_streaming_logged"] = False
1477
+
1478
+ elif event_type == "new_answer":
1479
+ label = context.get("label")
1480
+ if label:
1481
+ agent_states[agent_id]["answer"] = label
1482
+ agent_states[agent_id]["status"] = "answered"
1483
+ agent_states[agent_id]["last_streaming_logged"] = False
1484
+ preview = self.agent_answers.get(agent_id, "")
1485
+ agent_states[agent_id]["preview"] = preview
1486
+ row = self._create_rich_event_row(
1487
+ event_num,
1488
+ agent_id,
1489
+ agent_states,
1490
+ "new_answer",
1491
+ label,
1492
+ preview,
1493
+ )
1494
+ if row:
1495
+ table.add_row(*row)
1496
+ event_num += 1
1497
+
1498
+ elif event_type == "vote_cast":
1499
+ vote = context.get("voted_for_label")
1500
+ reason = context.get("reason", "")
1501
+ if vote:
1502
+ agent_states[agent_id]["vote"] = vote
1503
+ agent_states[agent_id]["status"] = "voted"
1504
+ agent_states[agent_id]["last_streaming_logged"] = False
1505
+ row = self._create_rich_event_row(
1506
+ event_num,
1507
+ agent_id,
1508
+ agent_states,
1509
+ "vote",
1510
+ vote,
1511
+ reason,
1512
+ )
1513
+ if row:
1514
+ table.add_row(*row)
1515
+ event_num += 1
1516
+
1517
+ elif event_type == "final_answer":
1518
+ label = context.get("label")
1519
+ if label:
1520
+ agent_states[agent_id]["status"] = "final"
1521
+ preview = agent_states[agent_id].get("preview", "")
1522
+ row = self._create_rich_event_row(
1523
+ event_num,
1524
+ agent_id,
1525
+ agent_states,
1526
+ "final_answer",
1527
+ label,
1528
+ preview,
1529
+ )
1530
+ if row:
1531
+ table.add_row(*row)
1532
+ event_num += 1
1533
+
1534
+ # Add summary section
1535
+ self._add_rich_summary(table, agent_states)
1536
+
1537
+ # Return both legend and table
1538
+ return (legend, table)
1539
+
1540
+ def _create_rich_event_row(
1541
+ self,
1542
+ event_num: int,
1543
+ active_agent: str,
1544
+ agent_states: Dict[str, Any],
1545
+ event_type: str,
1546
+ *args: Any,
1547
+ ) -> list:
1548
+ """Create a rich table row for an event"""
1549
+ row = [f"[bold yellow]E{event_num}[/bold yellow]"]
1550
+
1551
+ for agent in self.agents:
1552
+ if agent == active_agent:
1553
+ # Active agent performing the event
1554
+ if event_type == "streaming_start":
1555
+ context = agent_states[agent]["context"]
1556
+ context_str = f"[dim blue]📋 Context: \\[{', '.join(context)}][/dim blue]\n" if context else "[dim blue]📋 Context: \\[][/dim blue]\n"
1557
+ cell = context_str + "[bold cyan]💭 Started streaming[/bold cyan]"
1558
+ elif event_type == "restart_completed":
1559
+ cell = f"[bold green]✅ RESTART COMPLETED (Restart {args[0]})[/bold green]"
1560
+ elif event_type == "new_answer":
1561
+ label, preview = args[0], args[1] if len(args) > 1 else ""
1562
+ cell = f"[bold green]✨ NEW ANSWER: {label}[/bold green]"
1563
+ if preview:
1564
+ clean_preview = preview.replace("\n", " ").strip()
1565
+ preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
1566
+ cell += f"\n[dim white]👁️ Preview: {preview_truncated}[/dim white]"
1567
+ elif event_type == "vote":
1568
+ vote, reason = args[0], args[1] if len(args) > 1 else ""
1569
+ cell = f"[bold cyan]🗳️ VOTE: {vote}[/bold cyan]"
1570
+ if reason:
1571
+ clean_reason = reason.replace("\n", " ").strip()
1572
+ reason_preview = clean_reason[:50] + "..." if len(clean_reason) > 50 else clean_reason
1573
+ cell += f"\n[italic dim]💭 Reason: {reason_preview}[/italic dim]"
1574
+ elif event_type == "final_answer":
1575
+ label, preview = args[0], args[1] if len(args) > 1 else ""
1576
+ cell = f"[bold green]🎯 FINAL ANSWER: {label}[/bold green]"
1577
+ if preview:
1578
+ clean_preview = preview.replace("\n", " ").strip()
1579
+ preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
1580
+ cell += f"\n[dim white]👁️ Preview: {preview_truncated}[/dim white]"
1581
+ else:
1582
+ cell = ""
1583
+ row.append(cell)
1584
+ else:
1585
+ # Other agents showing status - prioritize active states
1586
+ status = agent_states[agent]["status"]
1587
+ if status in ["streaming", "answering"]:
1588
+ cell = f"[cyan]🔄 ({status})[/cyan]"
1589
+ elif status == "voted":
1590
+ cell = "[green]✅ (voted)[/green]"
1591
+ elif status == "answered":
1592
+ if agent_states[agent]["answer"]:
1593
+ cell = f"[green]✅ Answered: {agent_states[agent]['answer']}[/green]"
1594
+ else:
1595
+ cell = "[green]✅ (answered)[/green]"
1596
+ elif status == "completed":
1597
+ cell = "[green]✅ (completed)[/green]"
1598
+ elif status == "final":
1599
+ cell = "[bold green]🎯 (final answer given)[/bold green]"
1600
+ elif status == "idle":
1601
+ cell = "[dim]⏳ (waiting)[/dim]"
1602
+ else:
1603
+ cell = f"[dim]({status})[/dim]"
1604
+ row.append(cell)
1605
+
1606
+ return row
1607
+
1608
+ def _add_rich_summary(self, table: Any, agent_states: dict) -> None:
1609
+ """Add summary statistics to the rich table"""
1610
+ # Calculate statistics
1611
+ total_answers = sum(1 for agent in self.agents if agent_states[agent]["answer"])
1612
+ total_votes = sum(1 for agent in self.agents if agent_states[agent]["vote"])
1613
+ total_restarts = len(
1614
+ [e for e in self.events if e["event_type"] == "restart_completed"],
1615
+ )
1616
+
1617
+ # Summary header
1618
+ summary_row = ["[bold magenta]SUMMARY[/bold magenta]"]
1619
+ for agent in self.agents:
1620
+ agent_num = self.agent_mapping.get(agent, "?")
1621
+ agent_name = f"Agent {agent_num}"
1622
+ summary_row.append(f"[bold magenta]{agent_name}[/bold magenta]")
1623
+ table.add_row(*summary_row)
1624
+
1625
+ # Stats for each agent
1626
+ stats_row = ["[bold]Stats[/bold]"]
1627
+ for agent in self.agents:
1628
+ answer_count = 1 if agent_states[agent]["answer"] else 0
1629
+ vote_count = 1 if agent_states[agent]["vote"] else 0
1630
+ restart_count = len(
1631
+ [e for e in self.events if e["event_type"] == "restart_completed" and e.get("agent_id") == agent],
1632
+ )
1633
+
1634
+ status = agent_states[agent]["status"]
1635
+ if status == "final":
1636
+ status_str = "[bold green]🏆 Winner[/bold green]"
1637
+ elif status == "completed":
1638
+ status_str = "[green]✅ Completed[/green]"
1639
+ else:
1640
+ status_str = f"[dim]{status}[/dim]"
1641
+
1642
+ stats = f"{answer_count} answer, {vote_count} vote, {restart_count} restarts\n{status_str}"
1643
+ stats_row.append(stats)
1644
+ table.add_row(*stats_row)
1645
+
1646
+ # Overall totals
1647
+ totals_row = ["[bold]TOTALS[/bold]"]
1648
+ totals_text = f"[bold cyan]{total_answers} answers, {total_votes} votes, {total_restarts} restarts[/bold cyan]"
1649
+ for _ in range(len(self.agents)):
1650
+ totals_row.append(totals_text)
1651
+ table.add_row(*totals_row)
1652
+
1653
+ def generate_rich_table(self) -> Optional["Table"]:
1654
+ """Generate a Rich table with proper formatting and colors."""
1655
+ if not RICH_AVAILABLE:
1656
+ return None
1657
+
1658
+ # Create main table with individual agent columns
1659
+ table = Table(
1660
+ box=box.DOUBLE_EDGE,
1661
+ show_header=True,
1662
+ header_style="bold bright_white on blue",
1663
+ expand=True,
1664
+ padding=(0, 1),
1665
+ title="[bold bright_cyan]Multi-Agent Coordination Flow[/bold bright_cyan]",
1666
+ title_style="bold bright_cyan",
1667
+ )
1668
+
1669
+ # Add columns with individual agents
1670
+ table.add_column(
1671
+ "Round",
1672
+ style="bold bright_white",
1673
+ width=14,
1674
+ justify="center",
1675
+ )
1676
+ for agent in self.agents:
1677
+ # Create readable agent names
1678
+ # Use the full agent name as provided by user configuration
1679
+ agent_name = agent
1680
+ # Use fixed width instead of ratio to prevent truncation
1681
+ table.add_column(
1682
+ agent_name,
1683
+ style="white",
1684
+ justify="center",
1685
+ width=40,
1686
+ overflow="fold",
1687
+ )
1688
+
1689
+ # Add user question row - create a nested table to achieve true
1690
+ # spanning
1691
+ from rich.table import Table as InnerTable
1692
+
1693
+ inner_question_table = InnerTable(
1694
+ box=None,
1695
+ show_header=False,
1696
+ expand=True,
1697
+ padding=(0, 0),
1698
+ )
1699
+ inner_question_table.add_column("Question", justify="center", ratio=1)
1700
+ inner_question_table.add_row(
1701
+ f"[bold bright_yellow]{self.user_question}[/bold bright_yellow]",
1702
+ )
1703
+
1704
+ question_cells = [""] # Empty round column
1705
+ question_cells.append(inner_question_table)
1706
+ # Fill remaining columns with empty strings - Rich will merge them
1707
+ # visually
1708
+ for i in range(len(self.agents) - 1):
1709
+ question_cells.append("")
1710
+ table.add_row(*question_cells)
1711
+
1712
+ # Add separator row
1713
+ separator_cells = [
1714
+ "[dim bright_blue]════════════[/dim bright_blue]",
1715
+ ] + ["[dim bright_blue]" + "═" * 88 + "[/dim bright_blue]" for _ in self.agents]
1716
+ table.add_row(*separator_cells)
1717
+
1718
+ # Process each round
1719
+ for i, round_data in enumerate(self.rounds):
1720
+ # Get content for each agent
1721
+ agent_contents = {}
1722
+ max_lines = 0
1723
+
1724
+ for agent in self.agents:
1725
+ content = self._build_rich_agent_cell_content(
1726
+ round_data.agent_states[agent],
1727
+ round_data.round_type,
1728
+ agent,
1729
+ round_data.round_num,
1730
+ )
1731
+ agent_contents[agent] = content
1732
+ max_lines = max(max_lines, len(content))
1733
+
1734
+ # Build round rows
1735
+ for line_idx in range(max_lines):
1736
+ row_cells = []
1737
+
1738
+ # Round label (only on first line)
1739
+ if line_idx == 0:
1740
+ if round_data.round_type == "FINAL":
1741
+ round_label = "[bold green]🏁 FINAL 🏁[/bold green]"
1742
+ else:
1743
+ round_label = f"[bold cyan]🔄 {round_data.round_type} 🔄[/bold cyan]"
1744
+ row_cells.append(round_label)
1745
+ else:
1746
+ row_cells.append("")
1747
+
1748
+ # Agent cells (individual columns)
1749
+ for agent in self.agents:
1750
+ content_lines = agent_contents[agent]
1751
+ if line_idx < len(content_lines):
1752
+ row_cells.append(content_lines[line_idx])
1753
+ else:
1754
+ row_cells.append("")
1755
+
1756
+ table.add_row(*row_cells)
1757
+
1758
+ # Round separator
1759
+ if i < len(self.rounds) - 1:
1760
+ next_round = self.rounds[i + 1]
1761
+ if next_round.round_type == "FINAL":
1762
+ # Winner announcement - simulate spanning
1763
+ if self.final_winner:
1764
+ # Use agent mapping for consistent naming
1765
+ agent_number = self.agent_mapping.get(
1766
+ self.final_winner,
1767
+ )
1768
+ if agent_number:
1769
+ winner_name = f"Agent {agent_number}"
1770
+ else:
1771
+ winner_name = self.final_winner
1772
+
1773
+ winner_announcement = f"🏆 {winner_name} selected as winner 🏆"
1774
+ # Create nested table for winner announcement spanning
1775
+ inner_winner_table = InnerTable(
1776
+ box=None,
1777
+ show_header=False,
1778
+ expand=True,
1779
+ padding=(0, 0),
1780
+ )
1781
+ inner_winner_table.add_column(
1782
+ "Winner",
1783
+ justify="center",
1784
+ ratio=1,
1785
+ )
1786
+ inner_winner_table.add_row(
1787
+ f"[bold bright_green]{winner_announcement}[/bold bright_green]",
1788
+ )
1789
+
1790
+ winner_cells = [""] # Empty round column
1791
+ winner_cells.append(inner_winner_table)
1792
+ # Fill remaining columns with empty strings
1793
+ for j in range(len(self.agents) - 1):
1794
+ winner_cells.append("")
1795
+ table.add_row(*winner_cells)
1796
+
1797
+ # Solid line before FINAL
1798
+ separator_cells = [
1799
+ "[dim green]────────────[/dim green]",
1800
+ ] + ["[dim green]" + "─" * 88 + "[/dim green]" for _ in self.agents]
1801
+ table.add_row(*separator_cells)
1802
+ else:
1803
+ # Wavy line between regular rounds
1804
+ separator_cells = ["[dim cyan]~~~~~~~~~~~~[/dim cyan]"] + ["[dim cyan]" + "~" * 88 + "[/dim cyan]" for _ in self.agents]
1805
+ table.add_row(*separator_cells)
1806
+
1807
+ return table
1808
+
1809
+ def _build_rich_agent_cell_content(
1810
+ self,
1811
+ agent_state: AgentState,
1812
+ round_type: str,
1813
+ agent_id: str,
1814
+ round_num: int,
1815
+ ) -> List[str]:
1816
+ """Build Rich-formatted content for an agent's cell in a round."""
1817
+ lines = []
1818
+
1819
+ # Determine if we should show context (for non-voting scenarios)
1820
+ show_context = (agent_state.current_answer and not agent_state.vote) or agent_state.has_final_answer or agent_state.status in ["streaming", "answering"]
1821
+
1822
+ # Don't show context for completed agents in FINAL round
1823
+ if round_type == "FINAL" and agent_state.status == "completed":
1824
+ show_context = False
1825
+
1826
+ # Add context with better styling (but not for voting agents)
1827
+ if show_context and not agent_state.vote:
1828
+ if agent_state.context:
1829
+ context_items = ", ".join(agent_state.context)
1830
+ # Escape brackets for Rich
1831
+ context_str = f"📋 Context: \\[{context_items}]"
1832
+ else:
1833
+ context_str = "📋 Context: \\[]" # Escape brackets for Rich
1834
+ lines.append(f"[dim blue]{context_str}[/dim blue]")
1835
+
1836
+ # Add content based on what happened in this round with enhanced
1837
+ # styling
1838
+ if agent_state.vote:
1839
+ # Agent voted in this round - always show context when voting
1840
+ if agent_state.context:
1841
+ context_items = ", ".join(agent_state.context)
1842
+ # Escape brackets for Rich
1843
+ context_str = f"📋 Context: \\[{context_items}]"
1844
+ lines.append(f"[dim blue]{context_str}[/dim blue]")
1845
+ vote_str = f"🗳️ VOTE: {agent_state.vote}"
1846
+ lines.append(f"[bold cyan]{vote_str}[/bold cyan]")
1847
+ if agent_state.vote_reason:
1848
+ # Clean up newlines and truncate
1849
+ clean_reason = agent_state.vote_reason.replace(
1850
+ "\n",
1851
+ " ",
1852
+ ).strip()
1853
+ reason = clean_reason[:65] + "..." if len(clean_reason) > 68 else clean_reason
1854
+ reason_str = f"💭 Reason: {reason}"
1855
+ lines.append(f"[italic dim]{reason_str}[/italic dim]")
1856
+
1857
+ elif round_type == "FINAL":
1858
+ # Final presentation round
1859
+ if agent_state.has_final_answer:
1860
+ final_str = f"🎯 FINAL ANSWER: {agent_state.current_answer}"
1861
+ lines.append(f"[bold green]{final_str}[/bold green]")
1862
+ if agent_state.answer_preview:
1863
+ # Clean up newlines in preview
1864
+ clean_preview = agent_state.answer_preview.replace(
1865
+ "\n",
1866
+ " ",
1867
+ ).strip()
1868
+ preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
1869
+ preview_str = f"👁️ Preview: {preview_truncated}"
1870
+ lines.append(f"[dim white]{preview_str}[/dim white]")
1871
+ else:
1872
+ lines.append(
1873
+ "[dim red]👁️ Preview: [Answer not available][/dim red]",
1874
+ )
1875
+ elif agent_state.status == "completed":
1876
+ lines.append("[dim green]✅ (completed)[/dim green]")
1877
+ else:
1878
+ lines.append("[dim yellow]⏳ (waiting)[/dim yellow]")
1879
+
1880
+ elif agent_state.current_answer and not agent_state.vote:
1881
+ # Agent provided an answer in this round
1882
+ answer_str = f"✨ NEW ANSWER: {agent_state.current_answer}"
1883
+ lines.append(f"[bold green]{answer_str}[/bold green]")
1884
+ if agent_state.answer_preview:
1885
+ # Clean up newlines in preview
1886
+ clean_preview = agent_state.answer_preview.replace(
1887
+ "\n",
1888
+ " ",
1889
+ ).strip()
1890
+ preview_truncated = clean_preview[:80] + "..." if len(clean_preview) > 80 else clean_preview
1891
+ preview_str = f"👁️ Preview: {preview_truncated}"
1892
+ lines.append(f"[dim white]{preview_str}[/dim white]")
1893
+ else:
1894
+ lines.append(
1895
+ "[dim red]👁️ Preview: [Answer not available][/dim red]",
1896
+ )
1897
+
1898
+ elif agent_state.status in ["streaming", "answering"]:
1899
+ lines.append("[bold yellow]🔄 (answering)[/bold yellow]")
1900
+
1901
+ elif agent_state.status == "voted":
1902
+ lines.append("[dim bright_cyan]✅ (voted)[/dim bright_cyan]")
1903
+
1904
+ elif agent_state.status == "answered":
1905
+ lines.append("[dim bright_green]✅ (answered)[/dim bright_green]")
1906
+
1907
+ else:
1908
+ lines.append("[dim]⏳ (waiting)[/dim]")
1909
+
1910
+ return lines
1911
+
1912
+
1913
+ def main() -> None:
1914
+ """Main entry point"""
1915
+ # Check for input file
1916
+ if len(sys.argv) > 1:
1917
+ filename = sys.argv[1]
1918
+ else:
1919
+ filename = "coordination_events.json"
1920
+
1921
+ try:
1922
+ # Load events
1923
+ with open(filename, "r") as f:
1924
+ events = json.load(f)
1925
+
1926
+ # Build and print table
1927
+ builder = CoordinationTableBuilder(events)
1928
+
1929
+ # Try to use Rich table first, fallback to plain text
1930
+ if RICH_AVAILABLE:
1931
+ rich_table = builder.generate_rich_event_table()
1932
+ if rich_table:
1933
+ console = Console()
1934
+ console.print(rich_table)
1935
+ else:
1936
+ # Fallback to plain event table
1937
+ table = builder.generate_event_table()
1938
+ print(table)
1939
+ else:
1940
+ # Use event-driven plain table as default
1941
+ table = builder.generate_event_table()
1942
+ print(table)
1943
+
1944
+ except FileNotFoundError:
1945
+ print(f"Error: Could not find file '{filename}'")
1946
+ sys.exit(1)
1947
+ except json.JSONDecodeError as e:
1948
+ print(f"Error: Invalid JSON in file '{filename}': {e}")
1949
+ sys.exit(1)
1950
+ except Exception as e:
1951
+ print(f"Error: {e}")
1952
+ sys.exit(1)
1953
+
1954
+
1955
+ if __name__ == "__main__":
1956
+ main()