massgen 0.0.3__py3-none-any.whl → 0.1.0__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 +1560 -275
  46. massgen/config_builder.py +2396 -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.0.dist-info/METADATA +1245 -0
  258. massgen-0.1.0.dist-info/RECORD +273 -0
  259. massgen-0.1.0.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.0.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,708 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Coordination Tracker for MassGen Orchestrator
4
+
5
+ This module provides comprehensive tracking of agent coordination events,
6
+ state transitions, and context sharing. It's integrated into the orchestrator
7
+ to capture the complete coordination flow for visualization and analysis.
8
+
9
+ The new approach is principled: we simply record what happens as it happens,
10
+ without trying to infer or manage state transitions. The orchestrator tells
11
+ us exactly what occurred and when.
12
+ """
13
+
14
+ import json
15
+ import time
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from .logger_config import logger
22
+ from .utils import ActionType, AgentStatus
23
+
24
+
25
+ class EventType(str, Enum):
26
+ SESSION_START = "session_start"
27
+ SESSION_END = "session_end"
28
+ ITERATION_START = "iteration_start"
29
+ ITERATION_END = "iteration_end"
30
+ STATUS_CHANGE = "status_change"
31
+ CONTEXT_RECEIVED = "context_received"
32
+ RESTART_TRIGGERED = "restart_triggered"
33
+ RESTART_COMPLETED = "restart_completed"
34
+ NEW_ANSWER = "new_answer"
35
+ VOTE_CAST = "vote_cast"
36
+ FINAL_AGENT_SELECTED = "final_agent_selected"
37
+ FINAL_ANSWER = "final_answer"
38
+ FINAL_ROUND_START = "final_round_start"
39
+
40
+ AGENT_ERROR = "agent_error"
41
+ AGENT_TIMEOUT = "agent_timeout"
42
+ AGENT_CANCELLED = "agent_cancelled"
43
+
44
+
45
+ ACTION_TO_EVENT = {
46
+ ActionType.ERROR: EventType.AGENT_ERROR,
47
+ ActionType.TIMEOUT: EventType.AGENT_TIMEOUT,
48
+ ActionType.CANCELLED: EventType.AGENT_CANCELLED,
49
+ }
50
+
51
+
52
+ @dataclass
53
+ class CoordinationEvent:
54
+ """A single coordination event with timestamp."""
55
+
56
+ timestamp: float
57
+ event_type: EventType
58
+ agent_id: Optional[str] = None
59
+ details: str = ""
60
+ context: Optional[Dict[str, Any]] = None
61
+
62
+ def to_dict(self) -> Dict[str, Any]:
63
+ """Convert to dictionary for serialization."""
64
+ return {
65
+ "timestamp": self.timestamp,
66
+ "event_type": self.event_type,
67
+ "agent_id": self.agent_id,
68
+ "details": self.details,
69
+ "context": self.context,
70
+ }
71
+
72
+
73
+ @dataclass
74
+ class AgentAnswer:
75
+ """Represents an answer from an agent."""
76
+
77
+ agent_id: str
78
+ content: str
79
+ timestamp: float
80
+
81
+ @property
82
+ def label(self) -> str:
83
+ """Auto-generate label based on answer properties."""
84
+ # This will be set by the tracker when it knows agent order
85
+ return getattr(self, "_label", "unknown")
86
+
87
+ @label.setter
88
+ def label(self, value: str):
89
+ self._label = value
90
+
91
+
92
+ @dataclass
93
+ class AgentVote:
94
+ """Represents a vote from an agent."""
95
+
96
+ voter_id: str
97
+ voted_for: str # Real agent ID like "gpt5nano_1"
98
+ voted_for_label: str # Answer label like "agent1.1"
99
+ voter_anon_id: str # Anonymous voter ID like "agent1"
100
+ reason: str
101
+ timestamp: float
102
+ available_answers: List[str] # Available answer labels like ["agent1.1", "agent2.1"]
103
+
104
+
105
+ class CoordinationTracker:
106
+ """
107
+ Principled coordination tracking that simply records what happens.
108
+
109
+ The orchestrator tells us exactly what occurred and when, without
110
+ us having to infer or manage complex state transitions.
111
+ """
112
+
113
+ def __init__(self):
114
+ # Event log - chronological record of everything that happens
115
+ self.events: List[CoordinationEvent] = []
116
+
117
+ # Answer tracking
118
+ self.answers_by_agent: Dict[str, List[AgentAnswer]] = {} # agent_id -> list of regular answers
119
+ self.final_answers: Dict[str, AgentAnswer] = {} # agent_id -> final answer
120
+
121
+ # Vote tracking
122
+ self.votes: List[AgentVote] = []
123
+
124
+ # Coordination iteration tracking
125
+ self.current_iteration: int = 0
126
+ self.agent_rounds: Dict[str, int] = {} # Per-agent round tracking - increments when restart completed
127
+ self.agent_round_context: Dict[str, Dict[int, List[str]]] = {} # What context each agent had in each round
128
+ self.iteration_available_labels: List[str] = [] # Frozen snapshot of available answer labels for current iteration
129
+
130
+ # Restart tracking - track pending restarts per agent
131
+ self.pending_agent_restarts: Dict[str, bool] = {} # agent_id -> is restart pending
132
+
133
+ # Session info
134
+ self.start_time: Optional[float] = None
135
+ self.end_time: Optional[float] = None
136
+ self.agent_ids: List[str] = []
137
+ self.final_winner: Optional[str] = None
138
+ self.final_context: Optional[Dict[str, Any]] = None # Context provided to final agent
139
+ self.is_final_round: bool = False # Track if we're in the final presentation round
140
+ self.user_prompt: Optional[str] = None # Store the initial user prompt
141
+
142
+ # Agent mappings - coordination tracker is the single source of truth
143
+ self.agent_context_labels: Dict[str, List[str]] = {} # Track what labels each agent can see
144
+
145
+ # Snapshot mapping - tracks filesystem snapshots for answers/votes
146
+ self.snapshot_mappings: Dict[str, Dict[str, Any]] = {} # label/vote_id -> snapshot info
147
+
148
+ def _make_snapshot_path(self, kind: str, agent_id: str, timestamp: str) -> str:
149
+ """Generate standardized snapshot paths.
150
+
151
+ Args:
152
+ kind: Type of snapshot ('answer', 'vote', 'final_answer', etc.)
153
+ agent_id: The agent ID
154
+ timestamp: The timestamp or 'final' for final answers
155
+
156
+ Returns:
157
+ The formatted path string
158
+ """
159
+ if kind == "final_answer" and timestamp == "final":
160
+ return f"final/{agent_id}/answer.txt"
161
+ if kind == "answer":
162
+ return f"{agent_id}/{timestamp}/answer.txt"
163
+ if kind == "vote":
164
+ return f"{agent_id}/{timestamp}/vote.json"
165
+ return f"{agent_id}/{timestamp}/{kind}.txt"
166
+
167
+ def initialize_session(self, agent_ids: List[str], user_prompt: Optional[str] = None):
168
+ """Initialize a new coordination session."""
169
+ self.start_time = time.time()
170
+ self.agent_ids = agent_ids.copy()
171
+ self.answers_by_agent = {aid: [] for aid in agent_ids}
172
+ self.user_prompt = user_prompt
173
+
174
+ # Initialize per-agent round tracking
175
+ self.agent_rounds = {aid: 0 for aid in agent_ids}
176
+ self.agent_round_context = {aid: {0: []} for aid in agent_ids} # Each agent starts in round 0 with empty context
177
+ self.pending_agent_restarts = {aid: False for aid in agent_ids}
178
+
179
+ # Initialize agent context tracking
180
+ self.agent_context_labels = {aid: [] for aid in agent_ids}
181
+
182
+ self._add_event(EventType.SESSION_START, None, f"Started with agents: {agent_ids}")
183
+
184
+ # Agent ID utility methods
185
+ def get_anonymous_id(self, agent_id: str) -> str:
186
+ """Get anonymous ID (agent1, agent2) for a full agent ID."""
187
+ agent_num = self._get_agent_number(agent_id)
188
+ return f"agent{agent_num}" if agent_num else agent_id
189
+
190
+ def _get_agent_number(self, agent_id: str) -> Optional[int]:
191
+ """Get the 1-based number for an agent (1, 2, 3, etc.)."""
192
+ if agent_id in self.agent_ids:
193
+ return self.agent_ids.index(agent_id) + 1
194
+ return None
195
+
196
+ def get_agent_context_labels(self, agent_id: str) -> List[str]:
197
+ """Get the answer labels this agent can currently see."""
198
+ return self.agent_context_labels.get(agent_id, []).copy()
199
+
200
+ def get_latest_answer_label(self, agent_id: str) -> Optional[str]:
201
+ """Get the latest answer label for an agent."""
202
+ if agent_id in self.answers_by_agent and self.answers_by_agent[agent_id]:
203
+ return self.answers_by_agent[agent_id][-1].label
204
+ return None
205
+
206
+ def get_agent_round(self, agent_id: str) -> int:
207
+ """Get the current round for a specific agent."""
208
+ return self.agent_rounds.get(agent_id, 0)
209
+
210
+ @property
211
+ def max_round(self) -> int:
212
+ """Get the highest round number across all agents."""
213
+ return max(self.agent_rounds.values()) if self.agent_rounds else 0
214
+
215
+ def start_new_iteration(self):
216
+ """Start a new coordination iteration."""
217
+ self.current_iteration += 1
218
+
219
+ # Capture available answer labels at start of this iteration (freeze snapshot)
220
+ self.iteration_available_labels = []
221
+ for agent_id, answers_list in self.answers_by_agent.items():
222
+ if answers_list: # Agent has provided at least one answer
223
+ latest_answer = answers_list[-1] # Get most recent answer
224
+ self.iteration_available_labels.append(latest_answer.label) # e.g., "agent1.1"
225
+
226
+ self._add_event(
227
+ EventType.ITERATION_START,
228
+ None,
229
+ f"Starting coordination iteration {self.current_iteration}",
230
+ {
231
+ "iteration": self.current_iteration,
232
+ "available_answers": self.iteration_available_labels.copy(),
233
+ },
234
+ )
235
+
236
+ def end_iteration(self, reason: str, details: Dict[str, Any] = None):
237
+ """Record how an iteration ended."""
238
+ context = {
239
+ "iteration": self.current_iteration,
240
+ "end_reason": reason,
241
+ "available_answers": self.iteration_available_labels.copy(),
242
+ }
243
+ if details:
244
+ context.update(details)
245
+
246
+ self._add_event(
247
+ EventType.ITERATION_END,
248
+ None,
249
+ f"Iteration {self.current_iteration} ended: {reason}",
250
+ context,
251
+ )
252
+
253
+ def set_user_prompt(self, prompt: str):
254
+ """Set or update the user prompt."""
255
+ self.user_prompt = prompt
256
+
257
+ def change_status(self, agent_id: str, new_status: AgentStatus):
258
+ """Record when an agent changes status."""
259
+ self._add_event(EventType.STATUS_CHANGE, agent_id, f"Changed to status: {new_status.value}")
260
+
261
+ def track_agent_context(
262
+ self,
263
+ agent_id: str,
264
+ answers: Dict[str, str],
265
+ conversation_history: Optional[Dict[str, Any]] = None,
266
+ agent_full_context: Optional[str] = None,
267
+ snapshot_dir: Optional[str] = None,
268
+ ):
269
+ """Record when an agent receives context.
270
+
271
+ Args:
272
+ agent_id: The agent receiving context
273
+ answers: Dict of agent_id -> answer content
274
+ conversation_history: Optional conversation history
275
+ agent_full_context: Optional full context string/dict to save
276
+ snapshot_dir: Optional directory path to save context.txt
277
+ """
278
+ # Convert full agent IDs to their corresponding answer labels using canonical mappings
279
+ answer_labels = []
280
+ for answering_agent_id in answers.keys():
281
+ if answering_agent_id in self.answers_by_agent and self.answers_by_agent[answering_agent_id]:
282
+ # Get the most recent answer's label
283
+ latest_answer = self.answers_by_agent[answering_agent_id][-1]
284
+ answer_labels.append(latest_answer.label)
285
+
286
+ # Update this agent's context labels using canonical mapping
287
+ self.agent_context_labels[agent_id] = answer_labels.copy()
288
+
289
+ # Use anonymous agent IDs for the event context
290
+ anon_answering_agents = [self.get_anonymous_id(aid) for aid in answers.keys()]
291
+
292
+ context = {
293
+ "available_answers": anon_answering_agents, # Anonymous IDs for backward compat
294
+ "available_answer_labels": answer_labels.copy(), # Store actual labels in event
295
+ "answer_count": len(answers),
296
+ "has_conversation_history": bool(conversation_history),
297
+ }
298
+ self._add_event(
299
+ EventType.CONTEXT_RECEIVED,
300
+ agent_id,
301
+ f"Received context with {len(answers)} answers",
302
+ context,
303
+ )
304
+
305
+ def track_restart_signal(self, triggering_agent: str, agents_restarted: List[str]):
306
+ """Record when a restart is triggered - but don't increment rounds yet."""
307
+ # Mark affected agents as having pending restarts
308
+ for agent_id in agents_restarted:
309
+ if True: # agent_id != triggering_agent: # Triggering agent doesn't restart themselves
310
+ self.pending_agent_restarts[agent_id] = True
311
+
312
+ # Log restart event (no round increment yet)
313
+ context = {
314
+ "affected_agents": agents_restarted,
315
+ "triggering_agent": triggering_agent,
316
+ }
317
+ self._add_event(
318
+ EventType.RESTART_TRIGGERED,
319
+ triggering_agent,
320
+ f"Triggered restart affecting {len(agents_restarted)} agents",
321
+ context,
322
+ )
323
+
324
+ def complete_agent_restart(self, agent_id: str):
325
+ """Record when an agent has completed its restart and increment their round.
326
+
327
+ Args:
328
+ agent_id: The agent that completed restart
329
+ """
330
+ if not self.pending_agent_restarts.get(agent_id, False):
331
+ # This agent wasn't pending a restart, nothing to do
332
+ return
333
+
334
+ # Mark restart as completed
335
+ self.pending_agent_restarts[agent_id] = False
336
+
337
+ # Increment this agent's round
338
+ self.agent_rounds[agent_id] += 1
339
+ new_round = self.agent_rounds[agent_id]
340
+
341
+ # Store the context this agent will work with in their new round
342
+ if agent_id not in self.agent_round_context:
343
+ self.agent_round_context[agent_id] = {}
344
+
345
+ # Log restart completion
346
+ context = {
347
+ "agent_round": new_round,
348
+ }
349
+ self._add_event(
350
+ EventType.RESTART_COMPLETED,
351
+ agent_id,
352
+ f"Completed restart - now in round {new_round}",
353
+ context,
354
+ )
355
+
356
+ def add_agent_answer(self, agent_id: str, answer: str, snapshot_timestamp: Optional[str] = None):
357
+ """Record when an agent provides a new answer.
358
+
359
+ Args:
360
+ agent_id: ID of the agent
361
+ answer: The answer content
362
+ snapshot_timestamp: Timestamp of the filesystem snapshot (if any)
363
+ """
364
+ # Create answer object
365
+ agent_answer = AgentAnswer(agent_id=agent_id, content=answer, timestamp=time.time())
366
+
367
+ # Auto-generate label based on agent position and answer count
368
+ agent_num = self._get_agent_number(agent_id)
369
+ answer_num = len(self.answers_by_agent[agent_id]) + 1
370
+ label = f"agent{agent_num}.{answer_num}"
371
+ agent_answer.label = label
372
+
373
+ # Store the answer
374
+ self.answers_by_agent[agent_id].append(agent_answer)
375
+
376
+ # Track snapshot mapping if provided
377
+ if snapshot_timestamp:
378
+ self.snapshot_mappings[label] = {
379
+ "type": "answer",
380
+ "label": label,
381
+ "agent_id": agent_id,
382
+ "timestamp": snapshot_timestamp,
383
+ "iteration": self.current_iteration,
384
+ "round": self.get_agent_round(agent_id),
385
+ "path": self._make_snapshot_path("answer", agent_id, snapshot_timestamp),
386
+ }
387
+
388
+ # Record event with label (important info) but no preview (that's for display only)
389
+ context = {"label": label}
390
+ self._add_event(EventType.NEW_ANSWER, agent_id, f"Provided answer {label}", context)
391
+
392
+ def add_agent_vote(
393
+ self,
394
+ agent_id: str,
395
+ vote_data: Dict[str, Any],
396
+ snapshot_timestamp: Optional[str] = None,
397
+ ):
398
+ """Record when an agent votes.
399
+
400
+ Args:
401
+ agent_id: ID of the voting agent
402
+ vote_data: Dictionary with vote information
403
+ snapshot_timestamp: Timestamp of the filesystem snapshot (if any)
404
+ """
405
+ # Handle both "voted_for" and "agent_id" keys (orchestrator uses "agent_id")
406
+ voted_for = vote_data.get("voted_for") or vote_data.get("agent_id", "unknown")
407
+ reason = vote_data.get("reason", "")
408
+
409
+ # Convert real agent IDs to anonymous IDs and answer labels
410
+ voter_anon_id = self.get_anonymous_id(agent_id)
411
+
412
+ # Find the voted-for answer label (agent1.1, agent2.1, etc.)
413
+ voted_for_label = "unknown"
414
+ if voted_for not in self.agent_ids:
415
+ logger.warning(f"Vote from {agent_id} for unknown agent {voted_for}")
416
+
417
+ if voted_for in self.agent_ids:
418
+ # Find the latest answer from the voted-for agent at vote time
419
+ voted_agent_answers = self.answers_by_agent.get(voted_for, [])
420
+ if voted_agent_answers:
421
+ voted_for_label = voted_agent_answers[-1].label
422
+
423
+ # Store the vote
424
+ vote = AgentVote(
425
+ voter_id=agent_id,
426
+ voted_for=voted_for,
427
+ voted_for_label=voted_for_label,
428
+ voter_anon_id=voter_anon_id,
429
+ reason=reason,
430
+ timestamp=time.time(),
431
+ available_answers=self.iteration_available_labels.copy(),
432
+ )
433
+ self.votes.append(vote)
434
+
435
+ # Track snapshot mapping if provided
436
+ if snapshot_timestamp:
437
+ # Create a meaningful vote label similar to answer labels
438
+ agent_num = self._get_agent_number(agent_id) or 0
439
+ vote_num = len([v for v in self.votes if v.voter_id == agent_id])
440
+ vote_label = f"agent{agent_num}.vote{vote_num}"
441
+
442
+ self.snapshot_mappings[vote_label] = {
443
+ "type": "vote",
444
+ "label": vote_label,
445
+ "agent_id": agent_id,
446
+ "timestamp": snapshot_timestamp,
447
+ "voted_for": voted_for,
448
+ "voted_for_label": voted_for_label,
449
+ "iteration": self.current_iteration,
450
+ "round": self.get_agent_round(agent_id),
451
+ "path": self._make_snapshot_path("vote", agent_id, snapshot_timestamp),
452
+ }
453
+
454
+ # Record event - only essential info in context
455
+ context = {
456
+ "voted_for": voted_for, # Real agent ID for compatibility
457
+ "voted_for_label": voted_for_label, # Answer label for display
458
+ "reason": reason,
459
+ "available_answers": self.iteration_available_labels.copy(),
460
+ }
461
+ self._add_event(EventType.VOTE_CAST, agent_id, f"Voted for {voted_for_label}", context)
462
+
463
+ def set_final_agent(self, agent_id: str, vote_summary: str, all_answers: Dict[str, str]):
464
+ """Record when final agent is selected."""
465
+ self.final_winner = agent_id
466
+
467
+ # Convert agent IDs to their answer labels
468
+ answer_labels = []
469
+ answers_with_labels = {}
470
+ for aid, answer_content in all_answers.items():
471
+ if aid in self.answers_by_agent and self.answers_by_agent[aid]:
472
+ # Get the latest answer label for this agent from regular answers
473
+ if self.answers_by_agent[aid]:
474
+ latest_answer = self.answers_by_agent[aid][-1]
475
+ answer_labels.append(latest_answer.label)
476
+ answers_with_labels[latest_answer.label] = answer_content
477
+
478
+ self.final_context = {
479
+ "vote_summary": vote_summary,
480
+ "all_answers": answer_labels, # Now contains labels like ["agent1.1", "agent2.1"]
481
+ "answers_for_context": answers_with_labels, # Now keyed by labels
482
+ }
483
+ self._add_event(
484
+ EventType.FINAL_AGENT_SELECTED,
485
+ agent_id,
486
+ "Selected as final presenter",
487
+ self.final_context,
488
+ )
489
+
490
+ def set_final_answer(self, agent_id: str, final_answer: str, snapshot_timestamp: Optional[str] = None):
491
+ """Record the final answer presentation.
492
+
493
+ Args:
494
+ agent_id: ID of the agent
495
+ final_answer: The final answer content
496
+ snapshot_timestamp: Timestamp of the filesystem snapshot (if any)
497
+ """
498
+ # Create final answer object
499
+ final_answer_obj = AgentAnswer(
500
+ agent_id=agent_id,
501
+ content=final_answer,
502
+ timestamp=time.time(),
503
+ )
504
+
505
+ # Auto-generate final label
506
+ agent_num = self._get_agent_number(agent_id)
507
+ label = f"agent{agent_num}.final"
508
+ final_answer_obj.label = label
509
+
510
+ # Store the final answer separately
511
+ self.final_answers[agent_id] = final_answer_obj
512
+
513
+ # Track snapshot mapping if provided
514
+ if snapshot_timestamp:
515
+ self.snapshot_mappings[label] = {
516
+ "type": "final_answer",
517
+ "label": label,
518
+ "agent_id": agent_id,
519
+ "timestamp": snapshot_timestamp,
520
+ "iteration": self.current_iteration,
521
+ "round": self.get_agent_round(agent_id),
522
+ "path": self._make_snapshot_path("final_answer", agent_id, snapshot_timestamp),
523
+ }
524
+
525
+ # Record event with label only (no preview)
526
+ context = {"label": label, **(self.final_context or {})}
527
+ self._add_event(EventType.FINAL_ANSWER, agent_id, f"Presented final answer {label}", context)
528
+
529
+ def start_final_round(self, selected_agent_id: str):
530
+ """Start the final presentation round."""
531
+ self.is_final_round = True
532
+ # Set the final round to be max round across all agents + 1
533
+ final_round = self.max_round + 1
534
+ self.agent_rounds[selected_agent_id] = final_round
535
+ self.final_winner = selected_agent_id
536
+
537
+ # Mark winner as starting final presentation
538
+ self.change_status(selected_agent_id, AgentStatus.STREAMING)
539
+
540
+ self._add_event(
541
+ EventType.FINAL_ROUND_START,
542
+ selected_agent_id,
543
+ f"Starting final presentation round {final_round}",
544
+ {"round_type": "final", "final_round": final_round},
545
+ )
546
+
547
+ def track_agent_action(self, agent_id: str, action_type, details: str = ""):
548
+ """Track any agent action using ActionType enum."""
549
+ if action_type == ActionType.NEW_ANSWER:
550
+ # For answers, details should be the actual answer content
551
+ self.add_agent_answer(agent_id, details)
552
+ elif action_type == ActionType.VOTE:
553
+ # For votes, details should be vote data dict - but this needs to be handled separately
554
+ # since add_agent_vote expects a dict, not a string
555
+ pass # Use add_agent_vote directly
556
+ else:
557
+ event_type = ACTION_TO_EVENT.get(action_type)
558
+ if event_type is None:
559
+ raise ValueError(f"Unsupported ActionType: {action_type}")
560
+ message = f"{action_type.value.upper()}: {details}" if details else action_type.value.upper()
561
+ self._add_event(event_type, agent_id, message)
562
+
563
+ def _add_event(
564
+ self,
565
+ event_type: EventType,
566
+ agent_id: Optional[str],
567
+ details: str,
568
+ context: Optional[Dict[str, Any]] = None,
569
+ ):
570
+ """Internal method to add an event."""
571
+ # Automatically include current iteration and round in context
572
+ if context is None:
573
+ context = {}
574
+ context = context.copy() # Don't modify the original
575
+ context["iteration"] = self.current_iteration
576
+
577
+ # Include agent-specific round if agent_id is provided, otherwise use max round
578
+ if agent_id:
579
+ context["round"] = self.get_agent_round(agent_id)
580
+ else:
581
+ context["round"] = self.max_round
582
+
583
+ event = CoordinationEvent(
584
+ timestamp=time.time(),
585
+ event_type=event_type,
586
+ agent_id=agent_id,
587
+ details=details,
588
+ context=context,
589
+ )
590
+ self.events.append(event)
591
+
592
+ def _end_session(self):
593
+ """Mark the end of the coordination session."""
594
+ self.end_time = time.time()
595
+ duration = self.end_time - (self.start_time or self.end_time)
596
+ self._add_event(EventType.SESSION_END, None, f"Session completed in {duration:.1f}s")
597
+
598
+ @property
599
+ def all_answers(self) -> Dict[str, str]:
600
+ """Get all answers as a label->content dictionary."""
601
+ result = {}
602
+ # Add regular answers
603
+ for answers in self.answers_by_agent.values():
604
+ for answer in answers:
605
+ result[answer.label] = answer.content
606
+ # Add final answers
607
+ for answer in self.final_answers.values():
608
+ result[answer.label] = answer.content
609
+ return result
610
+
611
+ def get_summary(self) -> Dict[str, Any]:
612
+ """Get session summary statistics."""
613
+ duration = (self.end_time or time.time()) - (self.start_time or time.time())
614
+ restart_count = len([e for e in self.events if e.event_type == EventType.RESTART_TRIGGERED])
615
+
616
+ return {
617
+ "duration": duration,
618
+ "total_events": len(self.events),
619
+ "total_restarts": restart_count,
620
+ "total_answers": sum(len(answers) for answers in self.answers_by_agent.values()),
621
+ "final_winner": self.final_winner,
622
+ "agent_count": len(self.agent_ids),
623
+ }
624
+
625
+ def save_coordination_logs(self, log_dir):
626
+ """Save all coordination data and create timeline visualization.
627
+
628
+ Args:
629
+ log_dir: Directory to save logs
630
+ format_style: "old", "new", or "both" (default)
631
+ """
632
+ try:
633
+ log_dir = Path(log_dir)
634
+ log_dir.mkdir(parents=True, exist_ok=True)
635
+
636
+ # Save raw events with session metadata
637
+ events_file = log_dir / "coordination_events.json"
638
+ with open(events_file, "w", encoding="utf-8") as f:
639
+ events_data = [event.to_dict() for event in self.events]
640
+
641
+ # Include session metadata at the beginning of the JSON
642
+ session_data = {
643
+ "session_metadata": {
644
+ "user_prompt": self.user_prompt,
645
+ "agent_ids": self.agent_ids,
646
+ "start_time": self.start_time,
647
+ "end_time": self.end_time,
648
+ "final_winner": self.final_winner,
649
+ },
650
+ "events": events_data,
651
+ }
652
+ json.dump(session_data, f, indent=2, default=str)
653
+
654
+ # Save snapshot mappings to track filesystem snapshots
655
+ if self.snapshot_mappings:
656
+ snapshot_mappings_file = log_dir / "snapshot_mappings.json"
657
+ with open(snapshot_mappings_file, "w", encoding="utf-8") as f:
658
+ json.dump(self.snapshot_mappings, f, indent=2, default=str)
659
+
660
+ # Generate coordination table using the new table generator
661
+ try:
662
+ self._generate_coordination_table(log_dir, session_data)
663
+ except Exception as e:
664
+ logger.warning(
665
+ f"Warning: Could not generate coordination table: {e}",
666
+ exc_info=True,
667
+ )
668
+
669
+ except Exception as e:
670
+ logger.warning(f"Failed to save coordination logs: {e}", exc_info=True)
671
+
672
+ def _generate_coordination_table(self, log_dir, session_data):
673
+ """Generate coordination table using the create_coordination_table.py module."""
674
+ try:
675
+ # Import the table builder
676
+ from massgen.frontend.displays.create_coordination_table import (
677
+ CoordinationTableBuilder,
678
+ )
679
+
680
+ # Create the event-driven table directly from session data (includes metadata)
681
+ builder = CoordinationTableBuilder(session_data)
682
+ table_content = builder.generate_event_table()
683
+
684
+ # Save the table to a file
685
+ table_file = log_dir / "coordination_table.txt"
686
+ with open(table_file, "w", encoding="utf-8") as f:
687
+ f.write(table_content)
688
+
689
+ logger.info(f"Coordination table generated at {table_file}")
690
+
691
+ except Exception as e:
692
+ logger.warning(f"Error generating coordination table: {e}", exc_info=True)
693
+
694
+ def _get_agent_id_from_label(self, label: str) -> str:
695
+ """Extract agent_id from a label like 'agent1.1' or 'agent2.final'."""
696
+ import re
697
+
698
+ match = re.match(r"agent(\d+)", label)
699
+ if match:
700
+ agent_num = int(match.group(1))
701
+ if 0 < agent_num <= len(self.agent_ids):
702
+ return self.agent_ids[agent_num - 1]
703
+ return "unknown"
704
+
705
+ def _get_agent_display_name(self, agent_id: str) -> str:
706
+ """Get display name for agent (Agent1, Agent2, etc.)."""
707
+ agent_num = self._get_agent_number(agent_id)
708
+ return f"Agent{agent_num}" if agent_num else agent_id