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.
- massgen/__init__.py +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|