massgen 0.1.0a2__py3-none-any.whl → 0.1.1__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 (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +643 -132
  20. massgen/config_builder.py +381 -32
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
massgen/config_builder.py CHANGED
@@ -25,7 +25,11 @@ from rich.prompt import Confirm, Prompt
25
25
  from rich.table import Table
26
26
  from rich.theme import Theme
27
27
 
28
- from massgen.backend.capabilities import BACKEND_CAPABILITIES, get_capabilities
28
+ from massgen.backend.capabilities import (
29
+ BACKEND_CAPABILITIES,
30
+ get_capabilities,
31
+ has_capability,
32
+ )
29
33
 
30
34
  # Load environment variables
31
35
  load_dotenv()
@@ -33,11 +37,11 @@ load_dotenv()
33
37
  # Custom theme for the CLI - using colors that work on both light and dark backgrounds
34
38
  custom_theme = Theme(
35
39
  {
36
- "info": "bright_blue",
37
- "warning": "bright_yellow",
38
- "error": "bright_red bold",
39
- "success": "bright_green bold",
40
- "prompt": "bright_magenta bold",
40
+ "info": "#4A90E2", # Medium blue - matches system status colors
41
+ "warning": "#CC6600", # Orange-brown - works on both light and dark
42
+ "error": "#CC0000 bold", # Deep red - strong contrast
43
+ "success": "#00AA44 bold", # Deep green - visible on both
44
+ "prompt": "#6633CC bold", # Purple - good on both backgrounds
41
45
  },
42
46
  )
43
47
 
@@ -790,12 +794,13 @@ class ConfigBuilder:
790
794
  console.print(f"[error]❌ Error configuring custom MCP: {e}[/error]")
791
795
  return None
792
796
 
793
- def batch_create_agents(self, count: int, provider_id: str) -> List[Dict]:
797
+ def batch_create_agents(self, count: int, provider_id: str, start_index: int = 0) -> List[Dict]:
794
798
  """Create multiple agents with the same provider.
795
799
 
796
800
  Args:
797
801
  count: Number of agents to create
798
802
  provider_id: Provider ID (e.g., 'openai', 'claude')
803
+ start_index: Starting index for agent naming (default: 0)
799
804
 
800
805
  Returns:
801
806
  List of agent configurations with default models
@@ -806,7 +811,7 @@ class ConfigBuilder:
806
811
  # Generate agent IDs like agent_a, agent_b, agent_c...
807
812
  for i in range(count):
808
813
  # Convert index to letter (0->a, 1->b, 2->c, etc.)
809
- agent_letter = chr(ord("a") + i)
814
+ agent_letter = chr(ord("a") + start_index + i)
810
815
 
811
816
  agent = {
812
817
  "id": f"agent_{agent_letter}",
@@ -816,20 +821,21 @@ class ConfigBuilder:
816
821
  },
817
822
  }
818
823
 
819
- # Add workspace for Claude Code (use numbers, not letters)
824
+ # Add workspace for Claude Code (use global index for unique workspace names)
820
825
  if provider_info.get("type") == "claude_code":
821
- agent["backend"]["cwd"] = f"workspace{i + 1}"
826
+ agent["backend"]["cwd"] = f"workspace{start_index + i + 1}"
822
827
 
823
828
  agents.append(agent)
824
829
 
825
830
  return agents
826
831
 
827
- def clone_agent(self, source_agent: Dict, new_id: str) -> Dict:
828
- """Clone an agent's configuration with a new ID.
832
+ def clone_agent(self, source_agent: Dict, new_id: str, target_backend_type: str = None) -> Dict:
833
+ """Clone an agent's configuration with a new ID, optionally preserving target backend.
829
834
 
830
835
  Args:
831
836
  source_agent: Agent to clone
832
837
  new_id: New agent ID
838
+ target_backend_type: If provided, preserve this backend type instead of copying source's
833
839
 
834
840
  Returns:
835
841
  Cloned agent with updated ID and workspace (if applicable)
@@ -839,9 +845,91 @@ class ConfigBuilder:
839
845
  cloned = copy.deepcopy(source_agent)
840
846
  cloned["id"] = new_id
841
847
 
842
- # Update workspace for Claude Code agents to avoid conflicts
843
- backend_type = cloned.get("backend", {}).get("type")
844
- if backend_type == "claude_code" and "cwd" in cloned.get("backend", {}):
848
+ # If target backend type is different, preserve it and update model
849
+ if target_backend_type and target_backend_type != source_agent.get("backend", {}).get("type"):
850
+ # Find target provider info to get default model
851
+ target_provider_info = None
852
+ for pid, pinfo in self.PROVIDERS.items():
853
+ if pinfo.get("type") == target_backend_type:
854
+ target_provider_info = pinfo
855
+ break
856
+
857
+ if target_provider_info:
858
+ # Preserve tool enablement flags (provider-agnostic)
859
+ preserved_settings = {}
860
+ skipped_settings = []
861
+ source_backend = source_agent.get("backend", {})
862
+ source_backend_type = source_backend.get("type")
863
+
864
+ # Copy filesystem settings (provider-agnostic)
865
+ if "cwd" in source_backend:
866
+ preserved_settings["cwd"] = source_backend["cwd"]
867
+
868
+ # Copy MCP servers (provider-agnostic, but check if target supports MCP)
869
+ if "mcp_servers" in source_backend:
870
+ # Check if target provider supports MCP
871
+ target_supports_mcp = "mcp" in target_provider_info.get("supports", [])
872
+ if target_supports_mcp:
873
+ preserved_settings["mcp_servers"] = copy.deepcopy(source_backend["mcp_servers"])
874
+ else:
875
+ skipped_settings.append("mcp_servers (not supported by target provider)")
876
+
877
+ # Copy tool flags if they exist and are supported by target
878
+ target_caps = get_capabilities(target_backend_type)
879
+
880
+ for key in [
881
+ "enable_web_search",
882
+ "enable_code_execution",
883
+ "enable_code_interpreter",
884
+ "enable_mcp_command_line",
885
+ "command_line_execution_mode",
886
+ ]:
887
+ if key in source_backend:
888
+ # Check if target supports this specific tool
889
+ if key == "enable_web_search":
890
+ if has_capability(target_backend_type, "web_search"):
891
+ preserved_settings[key] = source_backend[key]
892
+ else:
893
+ skipped_settings.append(f"{key} (not supported by {target_backend_type})")
894
+ elif key == "enable_code_interpreter":
895
+ # code_interpreter is OpenAI/Azure-specific
896
+ if target_caps and "code_interpreter" in target_caps.builtin_tools:
897
+ preserved_settings[key] = source_backend[key]
898
+ else:
899
+ skipped_settings.append(f"{key} (not supported by {target_backend_type})")
900
+ elif key == "enable_code_execution":
901
+ # code_execution is Claude/Gemini-specific
902
+ if target_caps and "code_execution" in target_caps.builtin_tools:
903
+ preserved_settings[key] = source_backend[key]
904
+ else:
905
+ skipped_settings.append(f"{key} (not supported by {target_backend_type})")
906
+ else:
907
+ # MCP command line and execution mode are universal
908
+ preserved_settings[key] = source_backend[key]
909
+
910
+ # Copy reasoning/text settings if target is OpenAI
911
+ if target_backend_type == "openai":
912
+ for key in ["text", "reasoning"]:
913
+ if key in source_backend:
914
+ preserved_settings[key] = copy.deepcopy(source_backend[key])
915
+ elif source_backend_type == "openai":
916
+ # Source was OpenAI but target is not - these settings can't be copied
917
+ for key in ["text", "reasoning"]:
918
+ if key in source_backend:
919
+ skipped_settings.append(f"{key} (OpenAI-specific)")
920
+
921
+ # Replace backend with target provider's default model + preserved settings
922
+ cloned["backend"] = {
923
+ "type": target_backend_type,
924
+ "model": target_provider_info.get("models", ["default"])[0],
925
+ **preserved_settings,
926
+ }
927
+
928
+ # Store skipped settings for later warning
929
+ cloned["_skipped_settings"] = skipped_settings
930
+
931
+ # Update workspace for filesystem-enabled agents to avoid conflicts
932
+ if "cwd" in cloned.get("backend", {}):
845
933
  # Extract number from new_id (e.g., "agent_b" -> 2)
846
934
  if "_" in new_id and len(new_id) > 0:
847
935
  agent_letter = new_id.split("_")[-1]
@@ -1049,12 +1137,13 @@ class ConfigBuilder:
1049
1137
  console.print(f"[error]❌ Error modifying agent: {e}[/error]")
1050
1138
  return agent
1051
1139
 
1052
- def apply_preset_to_agent(self, agent: Dict, use_case: str) -> Dict:
1140
+ def apply_preset_to_agent(self, agent: Dict, use_case: str, agent_index: int = 1) -> Dict:
1053
1141
  """Auto-apply preset configuration to an agent.
1054
1142
 
1055
1143
  Args:
1056
1144
  agent: Agent configuration dict
1057
1145
  use_case: Use case ID for preset configuration
1146
+ agent_index: Agent index for unique workspace naming (1-based)
1058
1147
 
1059
1148
  Returns:
1060
1149
  Updated agent configuration with preset applied
@@ -1080,7 +1169,8 @@ class ConfigBuilder:
1080
1169
  # Auto-enable filesystem if recommended
1081
1170
  if "filesystem" in recommended_tools and "filesystem" in provider_info.get("supports", []):
1082
1171
  if not agent["backend"].get("cwd"):
1083
- agent["backend"]["cwd"] = "workspace"
1172
+ # Generate unique workspace name for each agent
1173
+ agent["backend"]["cwd"] = f"workspace{agent_index}"
1084
1174
 
1085
1175
  # Auto-enable web search if recommended
1086
1176
  if "web_search" in recommended_tools:
@@ -1673,7 +1763,7 @@ class ConfigBuilder:
1673
1763
  if not provider_id:
1674
1764
  provider_id = available_providers[0]
1675
1765
 
1676
- agent_batch = self.batch_create_agents(1, provider_id)
1766
+ agent_batch = self.batch_create_agents(1, provider_id, len(agents))
1677
1767
  agents.extend(agent_batch)
1678
1768
 
1679
1769
  provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
@@ -1843,7 +1933,7 @@ class ConfigBuilder:
1843
1933
  console.print()
1844
1934
  console.print(" [cyan]Applying preset configuration to all agents...[/cyan]")
1845
1935
  for i, agent in enumerate(agents):
1846
- agents[i] = self.apply_preset_to_agent(agent, use_case)
1936
+ agents[i] = self.apply_preset_to_agent(agent, use_case, agent_index=i + 1)
1847
1937
 
1848
1938
  console.print(f" [green]✅ {len(agents)} agent(s) configured with preset[/green]")
1849
1939
  console.print()
@@ -1878,17 +1968,107 @@ class ConfigBuilder:
1878
1968
  ).ask()
1879
1969
 
1880
1970
  if clone_choice == "clone":
1881
- # Clone the previous agent
1971
+ # Clone the previous agent, preserving current agent's backend type
1882
1972
  source_agent = agents[i - 2]
1883
- agent = self.clone_agent(source_agent, agent["id"])
1973
+ target_backend_type = agent.get("backend", {}).get("type")
1974
+ source_backend_type = source_agent.get("backend", {}).get("type")
1975
+
1976
+ agent = self.clone_agent(source_agent, agent["id"], target_backend_type)
1977
+
1978
+ # If cross-provider cloning, prompt for model selection
1979
+ if target_backend_type != source_backend_type:
1980
+ console.print(f"✅ Cloned settings from agent_{chr(ord('a') + i - 2)} ({source_backend_type})")
1981
+ console.print(f" [dim]Note: Model must be selected for {target_backend_type}[/dim]")
1982
+
1983
+ # Show skipped settings warning if any
1984
+ skipped = agent.get("_skipped_settings", [])
1985
+ if skipped:
1986
+ console.print(" [yellow]⚠️ Skipped incompatible settings:[/yellow]")
1987
+ for setting in skipped:
1988
+ console.print(f" • {setting}")
1989
+ console.print()
1990
+
1991
+ # Prompt for model selection
1992
+ target_provider_info = None
1993
+ for _, pinfo in self.PROVIDERS.items():
1994
+ if pinfo.get("type") == target_backend_type:
1995
+ target_provider_info = pinfo
1996
+ break
1997
+
1998
+ if target_provider_info and target_provider_info.get("models"):
1999
+ model_choice = questionary.select(
2000
+ f"Select {target_backend_type} model:",
2001
+ choices=target_provider_info["models"],
2002
+ style=questionary.Style(
2003
+ [
2004
+ ("selected", "fg:cyan bold"),
2005
+ ("pointer", "fg:cyan bold"),
2006
+ ],
2007
+ ),
2008
+ ).ask()
2009
+
2010
+ if model_choice:
2011
+ agent["backend"]["model"] = model_choice
2012
+ console.print(f" ✅ Model: {model_choice}")
2013
+
2014
+ # Clean up temporary skipped settings marker
2015
+ agent.pop("_skipped_settings", None)
2016
+ else:
2017
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
2018
+ # Clean up temporary skipped settings marker
2019
+ agent.pop("_skipped_settings", None)
2020
+
1884
2021
  agents[i - 1] = agent
1885
- console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1886
2022
  console.print()
1887
2023
  continue
1888
2024
  elif clone_choice == "clone_modify":
1889
- # Clone and selectively modify
2025
+ # Clone and selectively modify, preserving current agent's backend type
1890
2026
  source_agent = agents[i - 2]
1891
- agent = self.clone_agent(source_agent, agent["id"])
2027
+ target_backend_type = agent.get("backend", {}).get("type")
2028
+ source_backend_type = source_agent.get("backend", {}).get("type")
2029
+
2030
+ agent = self.clone_agent(source_agent, agent["id"], target_backend_type)
2031
+
2032
+ # If cross-provider cloning, prompt for model selection before modification
2033
+ if target_backend_type != source_backend_type:
2034
+ console.print(f"✅ Cloned settings from agent_{chr(ord('a') + i - 2)} ({source_backend_type})")
2035
+ console.print(f" [dim]Note: Model must be selected for {target_backend_type}[/dim]")
2036
+
2037
+ # Show skipped settings warning if any
2038
+ skipped = agent.get("_skipped_settings", [])
2039
+ if skipped:
2040
+ console.print(" [yellow]⚠️ Skipped incompatible settings:[/yellow]")
2041
+ for setting in skipped:
2042
+ console.print(f" • {setting}")
2043
+ console.print()
2044
+
2045
+ # Prompt for model selection
2046
+ target_provider_info = None
2047
+ for _, pinfo in self.PROVIDERS.items():
2048
+ if pinfo.get("type") == target_backend_type:
2049
+ target_provider_info = pinfo
2050
+ break
2051
+
2052
+ if target_provider_info and target_provider_info.get("models"):
2053
+ model_choice = questionary.select(
2054
+ f"Select {target_backend_type} model:",
2055
+ choices=target_provider_info["models"],
2056
+ style=questionary.Style(
2057
+ [
2058
+ ("selected", "fg:cyan bold"),
2059
+ ("pointer", "fg:cyan bold"),
2060
+ ],
2061
+ ),
2062
+ ).ask()
2063
+
2064
+ if model_choice:
2065
+ agent["backend"]["model"] = model_choice
2066
+ console.print(f" ✅ Model: {model_choice}")
2067
+ console.print()
2068
+
2069
+ # Clean up temporary skipped settings marker before modification
2070
+ agent.pop("_skipped_settings", None)
2071
+
1892
2072
  agent = self.modify_cloned_agent(agent, i)
1893
2073
  agents[i - 1] = agent
1894
2074
  continue
@@ -1922,17 +2102,107 @@ class ConfigBuilder:
1922
2102
  ).ask()
1923
2103
 
1924
2104
  if clone_choice == "clone":
1925
- # Clone the previous agent
2105
+ # Clone the previous agent, preserving current agent's backend type
1926
2106
  source_agent = agents[i - 2]
1927
- agent = self.clone_agent(source_agent, agent["id"])
2107
+ target_backend_type = agent.get("backend", {}).get("type")
2108
+ source_backend_type = source_agent.get("backend", {}).get("type")
2109
+
2110
+ agent = self.clone_agent(source_agent, agent["id"], target_backend_type)
2111
+
2112
+ # If cross-provider cloning, prompt for model selection
2113
+ if target_backend_type != source_backend_type:
2114
+ console.print(f"✅ Cloned settings from agent_{chr(ord('a') + i - 2)} ({source_backend_type})")
2115
+ console.print(f" [dim]Note: Model must be selected for {target_backend_type}[/dim]")
2116
+
2117
+ # Show skipped settings warning if any
2118
+ skipped = agent.get("_skipped_settings", [])
2119
+ if skipped:
2120
+ console.print(" [yellow]⚠️ Skipped incompatible settings:[/yellow]")
2121
+ for setting in skipped:
2122
+ console.print(f" • {setting}")
2123
+ console.print()
2124
+
2125
+ # Prompt for model selection
2126
+ target_provider_info = None
2127
+ for _, pinfo in self.PROVIDERS.items():
2128
+ if pinfo.get("type") == target_backend_type:
2129
+ target_provider_info = pinfo
2130
+ break
2131
+
2132
+ if target_provider_info and target_provider_info.get("models"):
2133
+ model_choice = questionary.select(
2134
+ f"Select {target_backend_type} model:",
2135
+ choices=target_provider_info["models"],
2136
+ style=questionary.Style(
2137
+ [
2138
+ ("selected", "fg:cyan bold"),
2139
+ ("pointer", "fg:cyan bold"),
2140
+ ],
2141
+ ),
2142
+ ).ask()
2143
+
2144
+ if model_choice:
2145
+ agent["backend"]["model"] = model_choice
2146
+ console.print(f" ✅ Model: {model_choice}")
2147
+
2148
+ # Clean up temporary skipped settings marker
2149
+ agent.pop("_skipped_settings", None)
2150
+ else:
2151
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
2152
+ # Clean up temporary skipped settings marker
2153
+ agent.pop("_skipped_settings", None)
2154
+
1928
2155
  agents[i - 1] = agent
1929
- console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1930
2156
  console.print()
1931
2157
  continue
1932
2158
  elif clone_choice == "clone_modify":
1933
- # Clone and selectively modify
2159
+ # Clone and selectively modify, preserving current agent's backend type
1934
2160
  source_agent = agents[i - 2]
1935
- agent = self.clone_agent(source_agent, agent["id"])
2161
+ target_backend_type = agent.get("backend", {}).get("type")
2162
+ source_backend_type = source_agent.get("backend", {}).get("type")
2163
+
2164
+ agent = self.clone_agent(source_agent, agent["id"], target_backend_type)
2165
+
2166
+ # If cross-provider cloning, prompt for model selection before modification
2167
+ if target_backend_type != source_backend_type:
2168
+ console.print(f"✅ Cloned settings from agent_{chr(ord('a') + i - 2)} ({source_backend_type})")
2169
+ console.print(f" [dim]Note: Model must be selected for {target_backend_type}[/dim]")
2170
+
2171
+ # Show skipped settings warning if any
2172
+ skipped = agent.get("_skipped_settings", [])
2173
+ if skipped:
2174
+ console.print(" [yellow]⚠️ Skipped incompatible settings:[/yellow]")
2175
+ for setting in skipped:
2176
+ console.print(f" • {setting}")
2177
+ console.print()
2178
+
2179
+ # Prompt for model selection
2180
+ target_provider_info = None
2181
+ for _, pinfo in self.PROVIDERS.items():
2182
+ if pinfo.get("type") == target_backend_type:
2183
+ target_provider_info = pinfo
2184
+ break
2185
+
2186
+ if target_provider_info and target_provider_info.get("models"):
2187
+ model_choice = questionary.select(
2188
+ f"Select {target_backend_type} model:",
2189
+ choices=target_provider_info["models"],
2190
+ style=questionary.Style(
2191
+ [
2192
+ ("selected", "fg:cyan bold"),
2193
+ ("pointer", "fg:cyan bold"),
2194
+ ],
2195
+ ),
2196
+ ).ask()
2197
+
2198
+ if model_choice:
2199
+ agent["backend"]["model"] = model_choice
2200
+ console.print(f" ✅ Model: {model_choice}")
2201
+ console.print()
2202
+
2203
+ # Clean up temporary skipped settings marker before modification
2204
+ agent.pop("_skipped_settings", None)
2205
+
1936
2206
  agent = self.modify_cloned_agent(agent, i)
1937
2207
  agents[i - 1] = agent
1938
2208
  continue
@@ -2051,6 +2321,80 @@ class ConfigBuilder:
2051
2321
  console.print()
2052
2322
  console.print(" ✅ Planning mode enabled - MCP tools will plan without executing during coordination")
2053
2323
 
2324
+ # Voting Sensitivity - only ask for multi-agent setups
2325
+ if len(agents) > 1:
2326
+ console.print()
2327
+ console.print(" [dim]Voting Sensitivity: Controls how agents reach consensus[/dim]")
2328
+ console.print(" [dim]• L: Lenient - Lower threshold for faster decisions (default)[/dim]")
2329
+ console.print(" [dim]• B: Balanced - Often requires more answers for consensus[/dim]")
2330
+ console.print(" [dim]• S: Strict - High standards, maximum quality (slowest)[/dim]")
2331
+ console.print()
2332
+
2333
+ voting_input = Prompt.ask(
2334
+ " [prompt]Voting sensitivity[/prompt]",
2335
+ choices=["l", "b", "s"],
2336
+ default="l",
2337
+ )
2338
+
2339
+ # Map input to full value
2340
+ voting_map = {"l": "lenient", "b": "balanced", "s": "strict"}
2341
+ voting_choice = voting_map[voting_input]
2342
+
2343
+ orchestrator_config["voting_sensitivity"] = voting_choice
2344
+ console.print()
2345
+ console.print(f" ✅ Voting sensitivity set to: {voting_choice}")
2346
+
2347
+ # Answer Count Limit
2348
+ console.print()
2349
+ console.print(" [dim]Answer Count Limit: Controls maximum new answers per agent[/dim]")
2350
+ console.print(" [dim]• Prevents endless coordination rounds[/dim]")
2351
+ console.print(" [dim]• After limit, agents can only vote (not provide new answers)[/dim]")
2352
+ console.print()
2353
+
2354
+ limit_input = Prompt.ask(
2355
+ " [prompt]Max new answers per agent (leave empty for unlimited)[/prompt]",
2356
+ default="",
2357
+ )
2358
+
2359
+ if limit_input.strip():
2360
+ try:
2361
+ answer_limit = int(limit_input)
2362
+ if answer_limit > 0:
2363
+ orchestrator_config["max_new_answers_per_agent"] = answer_limit
2364
+ console.print()
2365
+ console.print(f" ✅ Answer limit set to: {answer_limit} per agent")
2366
+ else:
2367
+ console.print()
2368
+ console.print(" ⚠️ Invalid limit - using unlimited")
2369
+ except ValueError:
2370
+ console.print()
2371
+ console.print(" ⚠️ Invalid number - using unlimited")
2372
+ else:
2373
+ console.print()
2374
+ console.print(" ✅ Answer limit: unlimited")
2375
+
2376
+ # Answer Novelty Requirement
2377
+ console.print()
2378
+ console.print(" [dim]Answer Novelty: Controls how different new answers must be[/dim]")
2379
+ console.print(" [dim]• L: Lenient - No similarity checks (default, fastest)[/dim]")
2380
+ console.print(" [dim]• B: Balanced - Reject if >70% overlap (prevents rephrasing)[/dim]")
2381
+ console.print(" [dim]• S: Strict - Reject if >50% overlap (requires new approaches)[/dim]")
2382
+ console.print()
2383
+
2384
+ novelty_input = Prompt.ask(
2385
+ " [prompt]Answer novelty requirement[/prompt]",
2386
+ choices=["l", "b", "s"],
2387
+ default="l",
2388
+ )
2389
+
2390
+ # Map input to full value
2391
+ novelty_map = {"l": "lenient", "b": "balanced", "s": "strict"}
2392
+ novelty_choice = novelty_map[novelty_input]
2393
+
2394
+ orchestrator_config["answer_novelty_requirement"] = novelty_choice
2395
+ console.print()
2396
+ console.print(f" ✅ Answer novelty requirement set to: {novelty_choice}")
2397
+
2054
2398
  return agents, orchestrator_config
2055
2399
 
2056
2400
  except (KeyboardInterrupt, EOFError):
@@ -2135,11 +2479,13 @@ class ConfigBuilder:
2135
2479
  default="1",
2136
2480
  )
2137
2481
 
2482
+ # Determine save directory
2483
+ save_dir = None
2138
2484
  if save_location == "2":
2139
2485
  # Save to ~/.config/massgen/agents/
2140
- agents_dir = Path.home() / ".config/massgen/agents"
2141
- agents_dir.mkdir(parents=True, exist_ok=True)
2142
- default_name = str(agents_dir / "my_massgen_config.yaml")
2486
+ save_dir = Path.home() / ".config/massgen/agents"
2487
+ save_dir.mkdir(parents=True, exist_ok=True)
2488
+ default_name = str(save_dir / "my_massgen_config.yaml")
2143
2489
 
2144
2490
  while True:
2145
2491
  try:
@@ -2157,7 +2503,10 @@ class ConfigBuilder:
2157
2503
  if not filename.endswith(".yaml"):
2158
2504
  filename += ".yaml"
2159
2505
 
2506
+ # Create filepath - if save_dir is set and filename is not absolute, join them
2160
2507
  filepath = Path(filename)
2508
+ if save_dir and not filepath.is_absolute():
2509
+ filepath = save_dir / filepath
2161
2510
 
2162
2511
  # Check if file exists
2163
2512
  if filepath.exists():