massgen 0.1.0a3__py3-none-any.whl → 0.1.2__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 (120) 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 +15 -2
  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 +83 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +286 -15
  10. massgen/backend/capabilities.py +6 -6
  11. massgen/backend/chat_completions.py +200 -103
  12. massgen/backend/claude.py +115 -18
  13. massgen/backend/claude_code.py +378 -14
  14. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  15. massgen/backend/gemini.py +1333 -1629
  16. massgen/backend/gemini_mcp_manager.py +545 -0
  17. massgen/backend/gemini_trackers.py +344 -0
  18. massgen/backend/gemini_utils.py +43 -0
  19. massgen/backend/grok.py +39 -6
  20. massgen/backend/response.py +147 -81
  21. massgen/cli.py +605 -110
  22. massgen/config_builder.py +376 -27
  23. massgen/configs/README.md +123 -80
  24. massgen/configs/basic/multi/three_agents_default.yaml +3 -3
  25. massgen/configs/basic/single/single_agent.yaml +1 -1
  26. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  29. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  30. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  31. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  34. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  35. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  36. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  39. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  40. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  41. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  42. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  46. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  47. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  52. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  56. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  57. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  60. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  61. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  62. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  65. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  66. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  67. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  68. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  69. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
  70. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
  71. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
  72. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
  73. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
  74. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  75. massgen/formatter/_chat_completions_formatter.py +104 -0
  76. massgen/formatter/_claude_formatter.py +120 -0
  77. massgen/formatter/_gemini_formatter.py +448 -0
  78. massgen/formatter/_response_formatter.py +88 -0
  79. massgen/frontend/coordination_ui.py +4 -2
  80. massgen/logger_config.py +35 -3
  81. massgen/message_templates.py +56 -6
  82. massgen/orchestrator.py +512 -16
  83. massgen/stream_chunk/base.py +3 -0
  84. massgen/tests/custom_tools_example.py +392 -0
  85. massgen/tests/mcp_test_server.py +17 -7
  86. massgen/tests/test_config_builder.py +423 -0
  87. massgen/tests/test_custom_tools.py +401 -0
  88. massgen/tests/test_intelligent_planning_mode.py +643 -0
  89. massgen/tests/test_tools.py +127 -0
  90. massgen/token_manager/token_manager.py +13 -4
  91. massgen/tool/README.md +935 -0
  92. massgen/tool/__init__.py +39 -0
  93. massgen/tool/_async_helpers.py +70 -0
  94. massgen/tool/_basic/__init__.py +8 -0
  95. massgen/tool/_basic/_two_num_tool.py +24 -0
  96. massgen/tool/_code_executors/__init__.py +10 -0
  97. massgen/tool/_code_executors/_python_executor.py +74 -0
  98. massgen/tool/_code_executors/_shell_executor.py +61 -0
  99. massgen/tool/_exceptions.py +39 -0
  100. massgen/tool/_file_handlers/__init__.py +10 -0
  101. massgen/tool/_file_handlers/_file_operations.py +218 -0
  102. massgen/tool/_manager.py +634 -0
  103. massgen/tool/_registered_tool.py +88 -0
  104. massgen/tool/_result.py +66 -0
  105. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  106. massgen/tool/docs/builtin_tools.md +681 -0
  107. massgen/tool/docs/exceptions.md +794 -0
  108. massgen/tool/docs/execution_results.md +691 -0
  109. massgen/tool/docs/manager.md +887 -0
  110. massgen/tool/docs/workflow_toolkits.md +529 -0
  111. massgen/tool/workflow_toolkits/__init__.py +57 -0
  112. massgen/tool/workflow_toolkits/base.py +55 -0
  113. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  114. massgen/tool/workflow_toolkits/vote.py +167 -0
  115. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/METADATA +87 -129
  116. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/RECORD +120 -44
  117. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/WHEEL +0 -0
  118. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/entry_points.txt +0 -0
  119. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/licenses/LICENSE +0 -0
  120. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/top_level.txt +0 -0
@@ -73,7 +73,9 @@ from ..logger_config import (
73
73
  log_backend_activity,
74
74
  log_backend_agent_message,
75
75
  log_stream_chunk,
76
+ logger,
76
77
  )
78
+ from ..tool import ToolManager
77
79
  from .base import FilesystemSupport, LLMBackend, StreamChunk
78
80
 
79
81
 
@@ -148,6 +150,38 @@ class ClaudeCodeBackend(LLMBackend):
148
150
 
149
151
  self._pending_system_prompt: Optional[str] = None # Windows-only workaround
150
152
 
153
+ # Custom tools support - initialize ToolManager if custom_tools are provided
154
+ self._custom_tool_manager: Optional[ToolManager] = None
155
+ custom_tools = kwargs.get("custom_tools", [])
156
+ if custom_tools:
157
+ self._custom_tool_manager = ToolManager()
158
+ self._register_custom_tools(custom_tools)
159
+
160
+ # Create SDK MCP Server from custom tools and inject into mcp_servers
161
+ sdk_mcp_server = self._create_sdk_mcp_server_from_custom_tools()
162
+ if sdk_mcp_server:
163
+ # Ensure mcp_servers exists in config
164
+ if "mcp_servers" not in self.config:
165
+ self.config["mcp_servers"] = {}
166
+
167
+ # Add SDK MCP server (convert to list format if dict format is used)
168
+ if isinstance(self.config["mcp_servers"], dict):
169
+ # Already in dict format
170
+ self.config["mcp_servers"]["massgen_custom_tools"] = sdk_mcp_server
171
+ elif isinstance(self.config["mcp_servers"], list):
172
+ # List format - add as special entry with SDK server marker
173
+ self.config["mcp_servers"].append(
174
+ {
175
+ "name": "massgen_custom_tools",
176
+ "__sdk_server__": sdk_mcp_server,
177
+ },
178
+ )
179
+ else:
180
+ # Initialize as dict with SDK server
181
+ self.config["mcp_servers"] = {"massgen_custom_tools": sdk_mcp_server}
182
+
183
+ logger.info(f"Registered SDK MCP server with {len(self._custom_tool_manager.registered_tools)} custom tools")
184
+
151
185
  def _setup_windows_subprocess_cleanup_suppression(self):
152
186
  """Comprehensive Windows subprocess cleanup warning suppression."""
153
187
  # All warning filters
@@ -405,6 +439,327 @@ class ClaudeCodeBackend(LLMBackend):
405
439
  # # Will integrate with PermissionManager
406
440
  # pass
407
441
 
442
+ def _register_custom_tools(self, custom_tools: List[Dict[str, Any]]) -> None:
443
+ """Register custom tools with the tool manager.
444
+
445
+ Supports flexible configuration:
446
+ - function: str | List[str]
447
+ - description: str (shared) | List[str] (1-to-1 mapping)
448
+ - preset_args: dict (shared) | List[dict] (1-to-1 mapping)
449
+
450
+ Examples:
451
+ # Single function
452
+ function: "my_func"
453
+ description: "My description"
454
+
455
+ # Multiple functions with shared description
456
+ function: ["func1", "func2"]
457
+ description: "Shared description"
458
+
459
+ # Multiple functions with individual descriptions
460
+ function: ["func1", "func2"]
461
+ description: ["Description 1", "Description 2"]
462
+
463
+ # Multiple functions with mixed (shared desc, individual args)
464
+ function: ["func1", "func2"]
465
+ description: "Shared description"
466
+ preset_args: [{"arg1": "val1"}, {"arg1": "val2"}]
467
+
468
+ Args:
469
+ custom_tools: List of custom tool configurations
470
+ """
471
+ if not self._custom_tool_manager:
472
+ logger.warning("Custom tool manager not initialized, cannot register tools")
473
+ return
474
+
475
+ # Collect unique categories and create them if needed
476
+ categories = set()
477
+ for tool_config in custom_tools:
478
+ if isinstance(tool_config, dict):
479
+ category = tool_config.get("category", "default")
480
+ if category != "default":
481
+ categories.add(category)
482
+
483
+ # Create categories that don't exist
484
+ for category in categories:
485
+ if category not in self._custom_tool_manager.tool_categories:
486
+ self._custom_tool_manager.setup_category(
487
+ category_name=category,
488
+ description=f"Custom {category} tools",
489
+ enabled=True,
490
+ )
491
+
492
+ # Register each custom tool
493
+ for tool_config in custom_tools:
494
+ try:
495
+ if isinstance(tool_config, dict):
496
+ # Extract base configuration
497
+ path = tool_config.get("path")
498
+ category = tool_config.get("category", "default")
499
+
500
+ # Normalize function field to list
501
+ func_field = tool_config.get("function")
502
+ if isinstance(func_field, str):
503
+ functions = [func_field]
504
+ elif isinstance(func_field, list):
505
+ functions = func_field
506
+ else:
507
+ logger.error(
508
+ f"Invalid function field type: {type(func_field)}. " f"Must be str or List[str].",
509
+ )
510
+ continue
511
+
512
+ if not functions:
513
+ logger.error("Empty function list in tool config")
514
+ continue
515
+
516
+ num_functions = len(functions)
517
+
518
+ # Process name field (can be str or List[str])
519
+ name_field = tool_config.get("name")
520
+ names = self._process_field_for_functions(
521
+ name_field,
522
+ num_functions,
523
+ "name",
524
+ )
525
+ if names is None:
526
+ continue # Validation error, skip this tool
527
+
528
+ # Process description field (can be str or List[str])
529
+ desc_field = tool_config.get("description")
530
+ descriptions = self._process_field_for_functions(
531
+ desc_field,
532
+ num_functions,
533
+ "description",
534
+ )
535
+ if descriptions is None:
536
+ continue # Validation error, skip this tool
537
+
538
+ # Process preset_args field (can be dict or List[dict])
539
+ preset_field = tool_config.get("preset_args")
540
+ preset_args_list = self._process_field_for_functions(
541
+ preset_field,
542
+ num_functions,
543
+ "preset_args",
544
+ )
545
+ if preset_args_list is None:
546
+ continue # Validation error, skip this tool
547
+
548
+ # Register each function with its corresponding values
549
+ for i, func in enumerate(functions):
550
+ # Load the function first if custom name is needed
551
+ if names[i] and names[i] != func:
552
+ # Need to load function and apply custom name
553
+ if path:
554
+ loaded_func = self._custom_tool_manager._load_function_from_path(path, func)
555
+ else:
556
+ loaded_func = self._custom_tool_manager._load_builtin_function(func)
557
+
558
+ if loaded_func is None:
559
+ logger.error(f"Could not load function '{func}' from path: {path}")
560
+ continue
561
+
562
+ # Apply custom name by modifying __name__ attribute
563
+ loaded_func.__name__ = names[i]
564
+
565
+ # Register with loaded function (no path needed)
566
+ self._custom_tool_manager.add_tool_function(
567
+ path=None,
568
+ func=loaded_func,
569
+ category=category,
570
+ preset_args=preset_args_list[i],
571
+ description=descriptions[i],
572
+ )
573
+ else:
574
+ # No custom name or same as function name, use normal registration
575
+ self._custom_tool_manager.add_tool_function(
576
+ path=path,
577
+ func=func,
578
+ category=category,
579
+ preset_args=preset_args_list[i],
580
+ description=descriptions[i],
581
+ )
582
+
583
+ # Use custom name for logging if provided
584
+ registered_name = names[i] if names[i] else func
585
+ logger.info(
586
+ f"Registered custom tool: {registered_name} from {path} " f"(category: {category}, " f"desc: '{descriptions[i][:50] if descriptions[i] else 'None'}...')",
587
+ )
588
+
589
+ except Exception as e:
590
+ func_name = tool_config.get("function", "unknown")
591
+ logger.error(
592
+ f"Failed to register custom tool {func_name}: {e}",
593
+ exc_info=True,
594
+ )
595
+
596
+ def _process_field_for_functions(
597
+ self,
598
+ field_value: Any,
599
+ num_functions: int,
600
+ field_name: str,
601
+ ) -> Optional[List[Any]]:
602
+ """Process a config field that can be a single value or list.
603
+
604
+ Conversion rules:
605
+ - None → [None, None, ...] (repeated num_functions times)
606
+ - Single value (not list) → [value, value, ...] (shared)
607
+ - List with matching length → use as-is (1-to-1 mapping)
608
+ - List with wrong length → ERROR (return None)
609
+
610
+ Args:
611
+ field_value: The field value from config
612
+ num_functions: Number of functions being registered
613
+ field_name: Name of the field (for error messages)
614
+
615
+ Returns:
616
+ List of values (one per function), or None if validation fails
617
+
618
+ Examples:
619
+ _process_field_for_functions(None, 3, "desc")
620
+ → [None, None, None]
621
+
622
+ _process_field_for_functions("shared", 3, "desc")
623
+ → ["shared", "shared", "shared"]
624
+
625
+ _process_field_for_functions(["a", "b", "c"], 3, "desc")
626
+ → ["a", "b", "c"]
627
+
628
+ _process_field_for_functions(["a", "b"], 3, "desc")
629
+ → None (error logged)
630
+ """
631
+ # Case 1: None or missing field → use None for all functions
632
+ if field_value is None:
633
+ return [None] * num_functions
634
+
635
+ # Case 2: Single value (not a list) → share across all functions
636
+ if not isinstance(field_value, list):
637
+ return [field_value] * num_functions
638
+
639
+ # Case 3: List value → must match function count exactly
640
+ if len(field_value) == num_functions:
641
+ return field_value
642
+ else:
643
+ # Length mismatch → validation error
644
+ logger.error(
645
+ f"Configuration error: {field_name} is a list with "
646
+ f"{len(field_value)} items, but there are {num_functions} functions. "
647
+ f"Either use a single value (shared) or a list with exactly "
648
+ f"{num_functions} items (1-to-1 mapping).",
649
+ )
650
+ return None
651
+
652
+ async def _execute_massgen_custom_tool(self, tool_name: str, args: dict) -> dict:
653
+ """Execute a MassGen custom tool and convert result to MCP format.
654
+
655
+ Args:
656
+ tool_name: Name of the custom tool to execute
657
+ args: Arguments for the tool
658
+
659
+ Returns:
660
+ MCP-formatted response with content blocks
661
+ """
662
+ if not self._custom_tool_manager:
663
+ return {
664
+ "content": [
665
+ {"type": "text", "text": "Error: Custom tool manager not initialized"},
666
+ ],
667
+ }
668
+
669
+ tool_request = {
670
+ "name": tool_name,
671
+ "input": args,
672
+ }
673
+
674
+ result_text = ""
675
+ try:
676
+ async for result in self._custom_tool_manager.execute_tool(tool_request):
677
+ # Accumulate ExecutionResult blocks
678
+ if hasattr(result, "output_blocks"):
679
+ for block in result.output_blocks:
680
+ if hasattr(block, "data"):
681
+ result_text += str(block.data)
682
+ elif hasattr(block, "content"):
683
+ result_text += str(block.content)
684
+ elif hasattr(result, "content"):
685
+ result_text += str(result.content)
686
+ else:
687
+ result_text += str(result)
688
+ except Exception as e:
689
+ logger.error(f"Error executing custom tool {tool_name}: {e}")
690
+ result_text = f"Error: {str(e)}"
691
+
692
+ # Return MCP format response
693
+ return {
694
+ "content": [
695
+ {"type": "text", "text": result_text or "Tool executed successfully"},
696
+ ],
697
+ }
698
+
699
+ def _create_sdk_mcp_server_from_custom_tools(self):
700
+ """Convert MassGen custom tools to SDK MCP Server.
701
+
702
+ Returns:
703
+ SDK MCP Server instance or None if no tools or SDK unavailable
704
+ """
705
+ if not self._custom_tool_manager:
706
+ return None
707
+
708
+ try:
709
+ from claude_agent_sdk import create_sdk_mcp_server, tool
710
+ except ImportError:
711
+ logger.warning("claude-agent-sdk not available, custom tools will not be registered")
712
+ return None
713
+
714
+ # Get all registered custom tools
715
+ tool_schemas = self._custom_tool_manager.fetch_tool_schemas()
716
+ if not tool_schemas:
717
+ logger.info("No custom tools to register")
718
+ return None
719
+
720
+ # Convert each tool to MCP tool format
721
+ mcp_tools = []
722
+ for tool_schema in tool_schemas:
723
+ try:
724
+ tool_name = tool_schema["function"]["name"]
725
+ tool_desc = tool_schema["function"].get("description", "")
726
+ tool_params = tool_schema["function"]["parameters"]
727
+
728
+ # Create async wrapper for MassGen tool
729
+ # Use default argument to capture tool_name in closure
730
+ async def tool_wrapper(args, tool_name=tool_name):
731
+ return await self._execute_massgen_custom_tool(tool_name, args)
732
+
733
+ # Register using SDK tool decorator
734
+ mcp_tool = tool(
735
+ name=tool_name,
736
+ description=tool_desc,
737
+ input_schema=tool_params,
738
+ )(tool_wrapper)
739
+
740
+ mcp_tools.append(mcp_tool)
741
+ logger.info(f"Converted custom tool to MCP: {tool_name}")
742
+
743
+ except Exception as e:
744
+ logger.error(f"Failed to convert tool {tool_schema.get('function', {}).get('name', 'unknown')} to MCP: {e}")
745
+
746
+ if not mcp_tools:
747
+ logger.warning("No custom tools successfully converted to MCP")
748
+ return None
749
+
750
+ # Create SDK MCP server
751
+ try:
752
+ sdk_mcp_server = create_sdk_mcp_server(
753
+ name="massgen_custom_tools",
754
+ version="1.0.0",
755
+ tools=mcp_tools,
756
+ )
757
+ logger.info(f"Created SDK MCP server with {len(mcp_tools)} custom tools")
758
+ return sdk_mcp_server
759
+ except Exception as e:
760
+ logger.error(f"Failed to create SDK MCP server: {e}")
761
+ return None
762
+
408
763
  def _build_system_prompt_with_workflow_tools(self, tools: List[Dict[str, Any]], base_system: Optional[str] = None) -> str:
409
764
  """Build system prompt that includes workflow tools information.
410
765
 
@@ -703,6 +1058,7 @@ class ClaudeCodeBackend(LLMBackend):
703
1058
  "api_key",
704
1059
  "allowed_tools",
705
1060
  "permission_mode",
1061
+ "custom_tools", # Handled separately via SDK MCP server conversion
706
1062
  }
707
1063
 
708
1064
  # Get cwd from filesystem manager (always available since we require it in __init__)
@@ -720,10 +1076,15 @@ class ClaudeCodeBackend(LLMBackend):
720
1076
  mcp_servers = options_kwargs["mcp_servers"]
721
1077
  if isinstance(mcp_servers, list):
722
1078
  for server in mcp_servers:
723
- if isinstance(server, dict) and "name" in server:
724
- # Create a copy and remove "name" key
725
- server_config = {k: v for k, v in server.items() if k != "name"}
726
- mcp_servers_dict[server["name"]] = server_config
1079
+ if isinstance(server, dict):
1080
+ if "__sdk_server__" in server:
1081
+ # SDK MCP Server object (created via create_sdk_mcp_server)
1082
+ server_name = server["name"]
1083
+ mcp_servers_dict[server_name] = server["__sdk_server__"]
1084
+ elif "name" in server:
1085
+ # Regular dictionary configuration
1086
+ server_config = {k: v for k, v in server.items() if k != "name"}
1087
+ mcp_servers_dict[server["name"]] = server_config
727
1088
  elif isinstance(mcp_servers, dict):
728
1089
  # Already in dict format
729
1090
  mcp_servers_dict = mcp_servers
@@ -805,6 +1166,19 @@ class ClaudeCodeBackend(LLMBackend):
805
1166
  )
806
1167
  # Merge constructor config with stream kwargs (stream kwargs take priority)
807
1168
  all_params = {**self.config, **kwargs}
1169
+
1170
+ # Extract system message from messages for append mode (always do this)
1171
+ # This must be done BEFORE checking if we have a client to ensure workflow_system_prompt is always defined
1172
+ system_msg = next((msg for msg in messages if msg.get("role") == "system"), None)
1173
+ if system_msg:
1174
+ system_content = system_msg.get("content", "") # noqa: E128
1175
+ else:
1176
+ system_content = ""
1177
+
1178
+ # Build system prompt with tools information
1179
+ # This must be done before any conditional paths to ensure it's always defined
1180
+ workflow_system_prompt = self._build_system_prompt_with_workflow_tools(tools or [], system_content)
1181
+
808
1182
  # Check if we already have a client
809
1183
  if self._client is not None:
810
1184
  client = self._client
@@ -830,16 +1204,6 @@ class ClaudeCodeBackend(LLMBackend):
830
1204
  disallowed_tools.append(tool)
831
1205
  all_params["disallowed_tools"] = disallowed_tools
832
1206
 
833
- # Extract system message from messages for append mode (always do this)
834
- system_msg = next((msg for msg in messages if msg.get("role") == "system"), None)
835
- if system_msg:
836
- system_content = system_msg.get("content", "") # noqa: E128
837
- else:
838
- system_content = ""
839
-
840
- # Build system prompt with tools information
841
- workflow_system_prompt = self._build_system_prompt_with_workflow_tools(tools or [], system_content)
842
-
843
1207
  # Windows-specific handling: detect complex prompts that cause subprocess hang
844
1208
  if sys.platform == "win32" and len(workflow_system_prompt) > 200:
845
1209
  # Windows with complex prompt: use post-connection delivery to avoid hang
@@ -61,7 +61,7 @@
61
61
  ```python
62
62
  # Fine-grained tool streaming (beta) - REQUIRES BETA CLIENT
63
63
  stream = client.beta.messages.create(
64
- model="claude-3-5-sonnet-20241022",
64
+ model="claude-3-5-sonnet-20250114",
65
65
  messages=[{"role": "user", "content": "Search and analyze..."}],
66
66
  tools=[
67
67
  {"type": "web_search_20250305"},
@@ -96,7 +96,7 @@ async_client = anthropic.AsyncAnthropic()
96
96
  # IMPORTANT: For code execution, use the beta client
97
97
  beta_client = anthropic.AsyncAnthropic()
98
98
  response = await beta_client.beta.messages.create(
99
- model="claude-3-5-sonnet-20241022",
99
+ model="claude-3-5-sonnet-20250114",
100
100
  tools=[{"type": "code_execution_20250522"}],
101
101
  headers={"anthropic-beta": "code-execution-2025-05-22"},
102
102
  messages=[...]
@@ -197,7 +197,7 @@ class ClaudeBackend(LLMBackend):
197
197
  headers["anthropic-beta"] = "code-execution-2025-05-22"
198
198
 
199
199
  stream = await self.client.beta.messages.create(
200
- model="claude-3-5-sonnet-20241022",
200
+ model="claude-3-5-sonnet-20250114",
201
201
  messages=messages,
202
202
  tools=combined_tools,
203
203
  headers=headers,