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,477 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Docker Container Manager for MassGen
4
+
5
+ Manages Docker containers for isolated command execution.
6
+ Provides strong filesystem isolation by executing commands inside containers
7
+ while keeping MCP servers on the host.
8
+ """
9
+
10
+ import logging
11
+ import threading
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Check if docker is available
19
+ try:
20
+ import docker
21
+ from docker.errors import DockerException, ImageNotFound, NotFound
22
+ from docker.models.containers import Container
23
+
24
+ DOCKER_AVAILABLE = True
25
+ except ImportError:
26
+ DOCKER_AVAILABLE = False
27
+ logger.warning("Docker Python library not available. Install with: pip install docker")
28
+
29
+
30
+ class DockerManager:
31
+ """
32
+ Manages Docker containers for isolated command execution.
33
+
34
+ Each agent gets a persistent container for the orchestration session:
35
+ - Volume mounts for workspace and context paths
36
+ - Network isolation (configurable)
37
+ - Resource limits (CPU, memory)
38
+ - Commands executed via docker exec
39
+ - State persists across turns (packages stay installed)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ image: str = "massgen/mcp-runtime:latest",
45
+ network_mode: str = "none",
46
+ memory_limit: Optional[str] = None,
47
+ cpu_limit: Optional[float] = None,
48
+ ):
49
+ """
50
+ Initialize Docker manager.
51
+
52
+ Args:
53
+ image: Docker image to use for containers
54
+ network_mode: Network mode (none/bridge/host)
55
+ memory_limit: Memory limit (e.g., "2g", "512m")
56
+ cpu_limit: CPU limit (e.g., 2.0 for 2 CPUs)
57
+
58
+ Raises:
59
+ RuntimeError: If Docker is not available or cannot connect
60
+ """
61
+ if not DOCKER_AVAILABLE:
62
+ raise RuntimeError("Docker Python library not available. Install with: pip install docker")
63
+
64
+ self.image = image
65
+ self.network_mode = network_mode
66
+ self.memory_limit = memory_limit
67
+ self.cpu_limit = cpu_limit
68
+
69
+ try:
70
+ self.client = docker.from_env()
71
+ # Test connection
72
+ self.client.ping()
73
+
74
+ # Get Docker version info for logging
75
+ version_info = self.client.version()
76
+ docker_version = version_info.get("Version", "unknown")
77
+ api_version = version_info.get("ApiVersion", "unknown")
78
+
79
+ logger.info("🐳 [Docker] Client initialized successfully")
80
+ logger.info(f" Docker version: {docker_version}")
81
+ logger.info(f" API version: {api_version}")
82
+ except DockerException as e:
83
+ logger.error(f"❌ [Docker] Failed to connect to Docker daemon: {e}")
84
+ raise RuntimeError(f"Failed to connect to Docker: {e}")
85
+
86
+ self.containers: Dict[str, Container] = {} # agent_id -> container
87
+
88
+ def ensure_image_exists(self) -> None:
89
+ """
90
+ Ensure the Docker image exists locally.
91
+
92
+ Pulls the image if not found locally.
93
+
94
+ Raises:
95
+ RuntimeError: If image cannot be pulled
96
+ """
97
+ try:
98
+ self.client.images.get(self.image)
99
+ logger.info(f"✅ [Docker] Image '{self.image}' found locally")
100
+ except ImageNotFound:
101
+ logger.info(f"📥 [Docker] Image '{self.image}' not found locally, pulling...")
102
+ try:
103
+ self.client.images.pull(self.image)
104
+ logger.info(f"✅ [Docker] Successfully pulled image '{self.image}'")
105
+ except DockerException as e:
106
+ raise RuntimeError(f"Failed to pull Docker image '{self.image}': {e}")
107
+
108
+ def create_container(
109
+ self,
110
+ agent_id: str,
111
+ workspace_path: Path,
112
+ temp_workspace_path: Optional[Path] = None,
113
+ context_paths: Optional[List[Dict[str, Any]]] = None,
114
+ ) -> str:
115
+ """
116
+ Create and start a persistent Docker container for an agent.
117
+
118
+ The container runs for the entire orchestration session and maintains state
119
+ across command executions (installed packages, generated files, etc.).
120
+
121
+ IMPORTANT: Paths are mounted at the SAME location as on the host to maintain
122
+ path transparency. The LLM sees identical paths whether in Docker or local mode.
123
+
124
+ Args:
125
+ agent_id: Unique identifier for the agent
126
+ workspace_path: Path to agent's workspace (mounted at same path, read-write)
127
+ temp_workspace_path: Path to shared temp workspace (mounted at same path, read-only)
128
+ context_paths: List of context path dicts with 'path', 'permission', and optional 'name' keys
129
+ (each mounted at its host path)
130
+
131
+ Returns:
132
+ Container ID
133
+
134
+ Raises:
135
+ RuntimeError: If container creation fails
136
+ """
137
+ if agent_id in self.containers:
138
+ logger.warning(f"⚠️ [Docker] Container for agent {agent_id} already exists")
139
+ return self.containers[agent_id].id
140
+
141
+ # Ensure image exists
142
+ self.ensure_image_exists()
143
+
144
+ # Check for and remove any existing container with the same name
145
+ container_name = f"massgen-{agent_id}"
146
+ try:
147
+ existing = self.client.containers.get(container_name)
148
+ logger.warning(
149
+ f"🔄 [Docker] Found existing container '{container_name}' (id: {existing.short_id}), removing it",
150
+ )
151
+ existing.remove(force=True)
152
+ except NotFound:
153
+ # No existing container, this is expected
154
+ pass
155
+ except DockerException as e:
156
+ logger.warning(f"⚠️ [Docker] Error checking for existing container '{container_name}': {e}")
157
+
158
+ logger.info(f"🐳 [Docker] Creating container for agent '{agent_id}'")
159
+ logger.info(f" Image: {self.image}")
160
+ logger.info(f" Network: {self.network_mode}")
161
+ if self.memory_limit:
162
+ logger.info(f" Memory limit: {self.memory_limit}")
163
+ if self.cpu_limit:
164
+ logger.info(f" CPU limit: {self.cpu_limit} cores")
165
+
166
+ # Build volume mounts
167
+ # IMPORTANT: Mount paths at the SAME location as on host to avoid path confusion
168
+ # This makes Docker completely transparent to the LLM - it sees identical paths
169
+ volumes = {}
170
+ mount_info = []
171
+
172
+ # Mount agent workspace (read-write) at the SAME path as host
173
+ workspace_path = workspace_path.resolve()
174
+ volumes[str(workspace_path)] = {"bind": str(workspace_path), "mode": "rw"}
175
+ mount_info.append(f" {workspace_path} ← {workspace_path} (rw)")
176
+
177
+ # Mount temp workspace (read-only) at the SAME path as host
178
+ if temp_workspace_path:
179
+ temp_workspace_path = temp_workspace_path.resolve()
180
+ volumes[str(temp_workspace_path)] = {"bind": str(temp_workspace_path), "mode": "ro"}
181
+ mount_info.append(f" {temp_workspace_path} ← {temp_workspace_path} (ro)")
182
+
183
+ # Mount context paths at the SAME paths as host
184
+ if context_paths:
185
+ for ctx_path_config in context_paths:
186
+ ctx_path = Path(ctx_path_config["path"]).resolve()
187
+ permission = ctx_path_config.get("permission", "read")
188
+ mode = "rw" if permission == "write" else "ro"
189
+
190
+ volumes[str(ctx_path)] = {"bind": str(ctx_path), "mode": mode}
191
+ mount_info.append(f" {ctx_path} ← {ctx_path} ({mode})")
192
+
193
+ # Log volume mounts
194
+ if mount_info:
195
+ logger.info(" Volume mounts:")
196
+ for mount_line in mount_info:
197
+ logger.info(mount_line)
198
+
199
+ # Build resource limits
200
+ resource_config = {}
201
+ if self.memory_limit:
202
+ resource_config["mem_limit"] = self.memory_limit
203
+ if self.cpu_limit:
204
+ resource_config["nano_cpus"] = int(self.cpu_limit * 1e9)
205
+
206
+ # Container configuration
207
+ container_config = {
208
+ "image": self.image,
209
+ "name": container_name,
210
+ "command": ["tail", "-f", "/dev/null"], # Keep container running
211
+ "detach": True,
212
+ "volumes": volumes,
213
+ "working_dir": str(workspace_path), # Use host workspace path
214
+ "network_mode": self.network_mode,
215
+ "auto_remove": False, # Manual cleanup for better control
216
+ "stdin_open": True,
217
+ "tty": True,
218
+ **resource_config,
219
+ }
220
+
221
+ try:
222
+ # Create and start container
223
+ container = self.client.containers.run(**container_config)
224
+ self.containers[agent_id] = container
225
+
226
+ # Get container info for logging
227
+ container.reload() # Refresh container state
228
+ status = container.status
229
+
230
+ logger.info("✅ [Docker] Container created successfully")
231
+ logger.info(f" Container ID: {container.short_id}")
232
+ logger.info(f" Container name: {container_name}")
233
+ logger.info(f" Status: {status}")
234
+
235
+ # Show how to inspect the container
236
+ logger.debug(f"💡 [Docker] Inspect container: docker inspect {container.short_id}")
237
+ logger.debug(f"💡 [Docker] View logs: docker logs {container.short_id}")
238
+ logger.debug(f"💡 [Docker] Execute commands: docker exec -it {container.short_id} /bin/bash")
239
+
240
+ return container.id
241
+
242
+ except DockerException as e:
243
+ logger.error(f"❌ [Docker] Failed to create container for agent {agent_id}: {e}")
244
+ raise RuntimeError(f"Failed to create Docker container for agent {agent_id}: {e}")
245
+
246
+ def get_container(self, agent_id: str) -> Optional[Container]:
247
+ """
248
+ Get container for an agent.
249
+
250
+ Args:
251
+ agent_id: Agent identifier
252
+
253
+ Returns:
254
+ Container object or None if not found
255
+ """
256
+ return self.containers.get(agent_id)
257
+
258
+ def exec_command(
259
+ self,
260
+ agent_id: str,
261
+ command: str,
262
+ workdir: Optional[str] = None,
263
+ timeout: Optional[int] = None,
264
+ ) -> Dict[str, Any]:
265
+ """
266
+ Execute a command inside the agent's container.
267
+
268
+ Args:
269
+ agent_id: Agent identifier
270
+ command: Command to execute (as string, will be run in shell)
271
+ workdir: Working directory (uses host path - same path is mounted in container)
272
+ timeout: Command timeout in seconds (implemented using threading)
273
+
274
+ Returns:
275
+ Dictionary with:
276
+ - success: bool (True if exit_code == 0)
277
+ - exit_code: int
278
+ - stdout: str
279
+ - stderr: str (combined with stdout in Docker exec)
280
+ - execution_time: float
281
+ - command: str
282
+ - work_dir: str
283
+
284
+ Raises:
285
+ ValueError: If container not found
286
+ RuntimeError: If execution fails
287
+ """
288
+ container = self.containers.get(agent_id)
289
+ if not container:
290
+ raise ValueError(f"No container found for agent {agent_id}")
291
+
292
+ # Default workdir is the container's default working dir (set to workspace_path at creation)
293
+ effective_workdir = workdir if workdir else None
294
+
295
+ try:
296
+ # Run command through shell to support pipes, redirects, etc.
297
+ exec_config = {
298
+ "cmd": ["/bin/sh", "-c", command],
299
+ "stdout": True,
300
+ "stderr": True,
301
+ }
302
+
303
+ if effective_workdir:
304
+ exec_config["workdir"] = effective_workdir
305
+
306
+ logger.debug(f"🔧 [Docker] Executing in container {container.short_id}: {command}")
307
+
308
+ start_time = time.time()
309
+
310
+ # Handle timeout using threading
311
+ if timeout:
312
+ result_container = {}
313
+ exception_container = {}
314
+
315
+ def run_exec():
316
+ try:
317
+ result_container["data"] = container.exec_run(**exec_config)
318
+ except Exception as e:
319
+ exception_container["error"] = e
320
+
321
+ thread = threading.Thread(target=run_exec)
322
+ thread.daemon = True
323
+ thread.start()
324
+ thread.join(timeout=timeout)
325
+
326
+ execution_time = time.time() - start_time
327
+
328
+ if thread.is_alive():
329
+ # Timeout occurred
330
+ logger.warning(f"⚠️ [Docker] Command timed out after {timeout}s: {command}")
331
+ return {
332
+ "success": False,
333
+ "exit_code": -1,
334
+ "stdout": "",
335
+ "stderr": f"Command timed out after {timeout} seconds",
336
+ "execution_time": execution_time,
337
+ "command": command,
338
+ "work_dir": effective_workdir or "(container default)",
339
+ }
340
+
341
+ if "error" in exception_container:
342
+ raise exception_container["error"]
343
+
344
+ exit_code, output = result_container["data"]
345
+ else:
346
+ # No timeout - execute directly
347
+ exit_code, output = container.exec_run(**exec_config)
348
+ execution_time = time.time() - start_time
349
+
350
+ # Docker exec_run combines stdout and stderr
351
+ output_str = output.decode("utf-8") if isinstance(output, bytes) else output
352
+
353
+ if exit_code != 0:
354
+ logger.debug(f"⚠️ [Docker] Command exited with code {exit_code}")
355
+
356
+ return {
357
+ "success": exit_code == 0,
358
+ "exit_code": exit_code,
359
+ "stdout": output_str,
360
+ "stderr": "", # Docker exec_run combines stdout/stderr
361
+ "execution_time": execution_time,
362
+ "command": command,
363
+ "work_dir": effective_workdir or "(container default)",
364
+ }
365
+
366
+ except DockerException as e:
367
+ logger.error(f"❌ [Docker] Failed to execute command in container: {e}")
368
+ raise RuntimeError(f"Failed to execute command in container: {e}")
369
+
370
+ def stop_container(self, agent_id: str, timeout: int = 10) -> None:
371
+ """
372
+ Stop a container gracefully.
373
+
374
+ Args:
375
+ agent_id: Agent identifier
376
+ timeout: Seconds to wait before killing
377
+
378
+ Raises:
379
+ ValueError: If container not found
380
+ """
381
+ container = self.containers.get(agent_id)
382
+ if not container:
383
+ raise ValueError(f"No container found for agent {agent_id}")
384
+
385
+ try:
386
+ logger.info(f"🛑 [Docker] Stopping container {container.short_id} for agent {agent_id}")
387
+ container.stop(timeout=timeout)
388
+ logger.info("✅ [Docker] Container stopped successfully")
389
+ except DockerException as e:
390
+ logger.error(f"❌ [Docker] Failed to stop container for agent {agent_id}: {e}")
391
+
392
+ def remove_container(self, agent_id: str, force: bool = False) -> None:
393
+ """
394
+ Remove a container.
395
+
396
+ Args:
397
+ agent_id: Agent identifier
398
+ force: Force removal even if running
399
+
400
+ Raises:
401
+ ValueError: If container not found
402
+ """
403
+ container = self.containers.get(agent_id)
404
+ if not container:
405
+ raise ValueError(f"No container found for agent {agent_id}")
406
+
407
+ try:
408
+ container_id = container.short_id
409
+ logger.info(f"🗑️ [Docker] Removing container {container_id} for agent {agent_id}")
410
+ container.remove(force=force)
411
+ del self.containers[agent_id]
412
+ logger.info("✅ [Docker] Container removed successfully")
413
+ except DockerException as e:
414
+ logger.error(f"❌ [Docker] Failed to remove container for agent {agent_id}: {e}")
415
+
416
+ def cleanup(self, agent_id: Optional[str] = None) -> None:
417
+ """
418
+ Clean up containers.
419
+
420
+ Args:
421
+ agent_id: If provided, cleanup specific agent. Otherwise cleanup all.
422
+ """
423
+ if agent_id:
424
+ # Cleanup specific agent
425
+ if agent_id in self.containers:
426
+ logger.info(f"🧹 [Docker] Cleaning up container for agent {agent_id}")
427
+ try:
428
+ self.stop_container(agent_id)
429
+ self.remove_container(agent_id, force=True)
430
+ except Exception as e:
431
+ logger.error(f"❌ [Docker] Error cleaning up container for agent {agent_id}: {e}")
432
+ else:
433
+ # Cleanup all containers
434
+ if self.containers:
435
+ logger.info(f"🧹 [Docker] Cleaning up {len(self.containers)} container(s)")
436
+ for aid in list(self.containers.keys()):
437
+ try:
438
+ self.stop_container(aid)
439
+ self.remove_container(aid, force=True)
440
+ except Exception as e:
441
+ logger.error(f"❌ [Docker] Error cleaning up container for agent {aid}: {e}")
442
+
443
+ def log_container_info(self, agent_id: str) -> None:
444
+ """
445
+ Log detailed container information (useful for debugging).
446
+
447
+ Args:
448
+ agent_id: Agent identifier
449
+ """
450
+ container = self.containers.get(agent_id)
451
+ if not container:
452
+ logger.warning(f"⚠️ [Docker] No container found for agent {agent_id}")
453
+ return
454
+
455
+ try:
456
+ container.reload() # Refresh state
457
+
458
+ logger.info(f"📊 [Docker] Container information for agent '{agent_id}':")
459
+ logger.info(f" ID: {container.short_id}")
460
+ logger.info(f" Name: {container.name}")
461
+ logger.info(f" Status: {container.status}")
462
+ logger.info(f" Network: {self.network_mode}")
463
+ if self.memory_limit:
464
+ logger.info(f" Memory limit: {self.memory_limit}")
465
+ if self.cpu_limit:
466
+ logger.info(f" CPU limit: {self.cpu_limit} cores")
467
+ except Exception as e:
468
+ logger.warning(f"⚠️ [Docker] Could not log container info: {e}")
469
+
470
+ def __del__(self):
471
+ """Cleanup all containers on deletion."""
472
+ try:
473
+ if hasattr(self, "containers") and self.containers:
474
+ self.cleanup()
475
+ except Exception:
476
+ # Silently fail during cleanup - already logged in cleanup()
477
+ pass