massgen 0.1.4__py3-none-any.whl → 0.1.6__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 (84) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
  3. massgen/backend/capabilities.py +39 -0
  4. massgen/backend/chat_completions.py +111 -197
  5. massgen/backend/claude.py +210 -181
  6. massgen/backend/gemini.py +1015 -1559
  7. massgen/backend/grok.py +3 -2
  8. massgen/backend/response.py +160 -220
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +399 -25
  11. massgen/config_builder.py +20 -54
  12. massgen/config_validator.py +931 -0
  13. massgen/configs/README.md +95 -10
  14. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  15. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  16. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  17. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  18. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  19. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
  20. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
  21. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
  22. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
  23. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
  24. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
  25. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
  26. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
  27. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
  28. massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
  29. massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
  30. massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
  31. massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
  32. massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
  33. massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
  34. massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
  35. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
  36. massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
  37. massgen/formatter/_gemini_formatter.py +61 -15
  38. massgen/memory/README.md +277 -0
  39. massgen/memory/__init__.py +26 -0
  40. massgen/memory/_base.py +193 -0
  41. massgen/memory/_compression.py +237 -0
  42. massgen/memory/_context_monitor.py +211 -0
  43. massgen/memory/_conversation.py +255 -0
  44. massgen/memory/_fact_extraction_prompts.py +333 -0
  45. massgen/memory/_mem0_adapters.py +257 -0
  46. massgen/memory/_persistent.py +687 -0
  47. massgen/memory/docker-compose.qdrant.yml +36 -0
  48. massgen/memory/docs/DESIGN.md +388 -0
  49. massgen/memory/docs/QUICKSTART.md +409 -0
  50. massgen/memory/docs/SUMMARY.md +319 -0
  51. massgen/memory/docs/agent_use_memory.md +408 -0
  52. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  53. massgen/memory/examples.py +237 -0
  54. massgen/orchestrator.py +207 -7
  55. massgen/tests/memory/test_agent_compression.py +174 -0
  56. massgen/tests/memory/test_context_window_management.py +286 -0
  57. massgen/tests/memory/test_force_compression.py +154 -0
  58. massgen/tests/memory/test_simple_compression.py +147 -0
  59. massgen/tests/test_ag2_lesson_planner.py +223 -0
  60. massgen/tests/test_agent_memory.py +534 -0
  61. massgen/tests/test_config_validator.py +1156 -0
  62. massgen/tests/test_conversation_memory.py +382 -0
  63. massgen/tests/test_langgraph_lesson_planner.py +223 -0
  64. massgen/tests/test_orchestrator_memory.py +620 -0
  65. massgen/tests/test_persistent_memory.py +435 -0
  66. massgen/token_manager/token_manager.py +6 -0
  67. massgen/tool/__init__.py +2 -9
  68. massgen/tool/_decorators.py +52 -0
  69. massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
  70. massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
  71. massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
  72. massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
  73. massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
  74. massgen/tool/_manager.py +102 -16
  75. massgen/tool/_registered_tool.py +3 -0
  76. massgen/tool/_result.py +3 -0
  77. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
  78. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
  79. massgen/backend/gemini_mcp_manager.py +0 -545
  80. massgen/backend/gemini_trackers.py +0 -344
  81. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
  82. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
  83. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
  84. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
massgen/backend/claude.py CHANGED
@@ -37,7 +37,12 @@ from ..formatter import ClaudeFormatter
37
37
  from ..logger_config import log_backend_agent_message, log_stream_chunk, logger
38
38
  from ..mcp_tools.backend_utils import MCPErrorHandler
39
39
  from .base import FilesystemSupport, StreamChunk
40
- from .base_with_custom_tool_and_mcp import CustomToolAndMCPBackend, UploadFileError
40
+ from .base_with_custom_tool_and_mcp import (
41
+ CustomToolAndMCPBackend,
42
+ CustomToolChunk,
43
+ ToolExecutionConfig,
44
+ UploadFileError,
45
+ )
41
46
 
42
47
 
43
48
  class ClaudeBackend(CustomToolAndMCPBackend):
@@ -521,6 +526,87 @@ class ClaudeBackend(CustomToolAndMCPBackend):
521
526
  async for chunk in self._process_stream(stream, all_params, agent_id):
522
527
  yield chunk
523
528
 
529
+ def _append_tool_result_message(
530
+ self,
531
+ updated_messages: List[Dict[str, Any]],
532
+ call: Dict[str, Any],
533
+ result: Any,
534
+ tool_type: str,
535
+ ) -> None:
536
+ """Append tool result to messages in Claude format.
537
+
538
+ Args:
539
+ updated_messages: Message list to append to
540
+ call: Tool call dictionary with call_id, name, arguments
541
+ result: Tool execution result
542
+ tool_type: "custom" or "mcp"
543
+
544
+ Note:
545
+ Claude uses tool_result format with tool_use_id.
546
+ """
547
+ tool_result_msg = {
548
+ "role": "user",
549
+ "content": [
550
+ {
551
+ "type": "tool_result",
552
+ "tool_use_id": call.get("call_id", "") or call.get("id", ""),
553
+ "content": str(result),
554
+ },
555
+ ],
556
+ }
557
+ updated_messages.append(tool_result_msg)
558
+
559
+ def _append_tool_error_message(
560
+ self,
561
+ updated_messages: List[Dict[str, Any]],
562
+ call: Dict[str, Any],
563
+ error_msg: str,
564
+ tool_type: str,
565
+ ) -> None:
566
+ """Append tool error to messages in Claude format.
567
+
568
+ Args:
569
+ updated_messages: Message list to append to
570
+ call: Tool call dictionary with call_id, name, arguments
571
+ error_msg: Error message string
572
+ tool_type: "custom" or "mcp"
573
+
574
+ Note:
575
+ Claude uses tool_result format with tool_use_id for errors too.
576
+ """
577
+ error_result_msg = {
578
+ "role": "user",
579
+ "content": [
580
+ {
581
+ "type": "tool_result",
582
+ "tool_use_id": call.get("call_id", "") or call.get("id", ""),
583
+ "content": error_msg,
584
+ },
585
+ ],
586
+ }
587
+ updated_messages.append(error_result_msg)
588
+
589
+ async def _execute_custom_tool(self, call: Dict[str, Any]) -> AsyncGenerator[CustomToolChunk, None]:
590
+ """Execute custom tool with streaming support - async generator for base class.
591
+
592
+ This method is called by _execute_tool_with_logging and yields CustomToolChunk
593
+ objects for intermediate streaming output. The base class detects the async
594
+ generator and streams intermediate results to users in real-time.
595
+
596
+ Args:
597
+ call: Tool call dictionary with name and arguments
598
+
599
+ Yields:
600
+ CustomToolChunk objects with streaming data
601
+
602
+ Note:
603
+ - Intermediate chunks (completed=False) are streamed to users in real-time
604
+ - Final chunk (completed=True) contains the accumulated result for message history
605
+ - The base class automatically handles extracting and displaying intermediate chunks
606
+ """
607
+ async for chunk in self.stream_custom_tool_execution(call):
608
+ yield chunk
609
+
524
610
  async def _stream_with_custom_and_mcp_tools(
525
611
  self,
526
612
  current_messages: List[Dict[str, Any]],
@@ -660,8 +746,9 @@ class ClaudeBackend(CustomToolAndMCPBackend):
660
746
  elif event.type == "message_delta":
661
747
  pass
662
748
  elif event.type == "message_stop":
663
- # Identify MCP, custom, and non-MCP/non-custom tool calls among current_tool_uses
664
- non_mcp_non_custom_tool_calls = []
749
+ captured_calls = []
750
+ tool_use_by_name: Dict[str, Dict[str, Any]] = {}
751
+
665
752
  if current_tool_uses:
666
753
  for tool_use in current_tool_uses.values():
667
754
  tool_name = tool_use.get("name", "")
@@ -675,39 +762,68 @@ class ClaudeBackend(CustomToolAndMCPBackend):
675
762
  except json.JSONDecodeError:
676
763
  parsed_input = {"raw_input": tool_input}
677
764
 
678
- if self.is_mcp_tool_call(tool_name):
679
- mcp_tool_calls.append(
680
- {
681
- "id": tool_use["id"],
682
- "type": "function",
683
- "function": {
684
- "name": tool_name,
685
- "arguments": parsed_input,
686
- },
765
+ captured_calls.append(
766
+ {
767
+ "name": tool_name,
768
+ "arguments": json.dumps(parsed_input) if isinstance(parsed_input, dict) else str(parsed_input),
769
+ "call_id": tool_use["id"],
770
+ },
771
+ )
772
+
773
+ # Store tool_use info for reconstruction
774
+ tool_use_by_name[tool_use["id"]] = {
775
+ "id": tool_use["id"],
776
+ "parsed_input": parsed_input,
777
+ }
778
+
779
+ # Use helper to categorize tool calls
780
+ if captured_calls:
781
+ categorized_mcp, categorized_custom, categorized_provider = self._categorize_tool_calls(captured_calls)
782
+
783
+ # Reconstruct Claude-specific format for each category
784
+ for call in categorized_mcp:
785
+ tool_info = tool_use_by_name[call["call_id"]]
786
+ mcp_tool_calls.append(
787
+ {
788
+ "id": tool_info["id"],
789
+ "type": "function",
790
+ "function": {
791
+ "name": call["name"],
792
+ "arguments": tool_info["parsed_input"],
687
793
  },
688
- )
689
- elif self.is_custom_tool_call(tool_name):
690
- custom_tool_calls.append(
691
- {
692
- "id": tool_use["id"],
693
- "type": "function",
694
- "function": {
695
- "name": tool_name,
696
- "arguments": parsed_input,
697
- },
794
+ },
795
+ )
796
+
797
+ for call in categorized_custom:
798
+ tool_info = tool_use_by_name[call["call_id"]]
799
+ custom_tool_calls.append(
800
+ {
801
+ "id": tool_info["id"],
802
+ "type": "function",
803
+ "function": {
804
+ "name": call["name"],
805
+ "arguments": tool_info["parsed_input"],
698
806
  },
699
- )
700
- else:
701
- non_mcp_non_custom_tool_calls.append(
702
- {
703
- "id": tool_use["id"],
704
- "type": "function",
705
- "function": {
706
- "name": tool_name,
707
- "arguments": parsed_input,
708
- },
807
+ },
808
+ )
809
+
810
+ # Build non-MCP/non-custom tool calls (including workflow tools)
811
+ non_mcp_non_custom_tool_calls = []
812
+ for call in categorized_provider:
813
+ tool_info = tool_use_by_name[call["call_id"]]
814
+ non_mcp_non_custom_tool_calls.append(
815
+ {
816
+ "id": tool_info["id"],
817
+ "type": "function",
818
+ "function": {
819
+ "name": call["name"],
820
+ "arguments": tool_info["parsed_input"],
709
821
  },
710
- )
822
+ },
823
+ )
824
+ else:
825
+ non_mcp_non_custom_tool_calls = []
826
+
711
827
  # Emit non-MCP/non-custom tool calls for the caller to execute
712
828
  if non_mcp_non_custom_tool_calls:
713
829
  log_stream_chunk("backend.claude", "tool_calls", non_mcp_non_custom_tool_calls, agent_id)
@@ -773,156 +889,72 @@ class ClaudeBackend(CustomToolAndMCPBackend):
773
889
  # Append the assistant message with tool uses
774
890
  updated_messages.append({"role": "assistant", "content": assistant_content})
775
891
 
776
- # First execute custom tool calls and append results
777
- for tool_call in custom_tool_calls:
778
- function_name = tool_call["function"]["name"]
892
+ # Configuration for custom tool execution
893
+ CUSTOM_TOOL_CONFIG = ToolExecutionConfig(
894
+ tool_type="custom",
895
+ chunk_type="custom_tool_status",
896
+ emoji_prefix="🔧 [Custom Tool]",
897
+ success_emoji="✅ [Custom Tool]",
898
+ error_emoji="❌ [Custom Tool Error]",
899
+ source_prefix="custom_",
900
+ status_called="custom_tool_called",
901
+ status_response="custom_tool_response",
902
+ status_error="custom_tool_error",
903
+ execution_callback=self._execute_custom_tool,
904
+ )
779
905
 
780
- # Yield custom tool call status
781
- yield StreamChunk(
782
- type="custom_tool_status",
783
- status="custom_tool_called",
784
- content=f"🔧 [Custom Tool] Calling {function_name}...",
785
- source=f"custom_{function_name}",
786
- )
906
+ # Configuration for MCP tool execution
907
+ MCP_TOOL_CONFIG = ToolExecutionConfig(
908
+ tool_type="mcp",
909
+ chunk_type="mcp_status",
910
+ emoji_prefix="🔧 [MCP Tool]",
911
+ success_emoji="✅ [MCP Tool]",
912
+ error_emoji="❌ [MCP Tool Error]",
913
+ source_prefix="mcp_",
914
+ status_called="mcp_tool_called",
915
+ status_response="mcp_tool_response",
916
+ status_error="mcp_tool_error",
917
+ execution_callback=self._execute_mcp_function_with_retry,
918
+ )
787
919
 
788
- try:
789
- # Execute custom function
790
- result_str = await self._execute_custom_tool(
791
- {
792
- "name": function_name,
793
- "arguments": json.dumps(tool_call["function"]["arguments"])
794
- if isinstance(tool_call["function"].get("arguments"), (dict, list))
795
- else tool_call["function"].get("arguments", "{}"),
796
- "call_id": tool_call["id"],
797
- },
798
- )
799
- if not result_str or result_str.startswith("Error:"):
800
- logger.warning(f"Custom function {function_name} failed: {result_str or 'unknown error'}")
801
- result_str = result_str or "Tool execution failed"
802
- except Exception as e:
803
- logger.error(f"Unexpected error in custom function execution: {e}")
804
- result_str = f"Error executing custom tool: {str(e)}"
805
-
806
- # Build tool result message
807
- tool_result_msg = {
808
- "role": "user",
809
- "content": [
810
- {
811
- "type": "tool_result",
812
- "tool_use_id": tool_call["id"],
813
- "content": result_str,
814
- },
815
- ],
920
+ def normalize_tool_call(tool_call: Dict[str, Any]) -> Dict[str, Any]:
921
+ """Convert Claude tool call format to unified format."""
922
+ return {
923
+ "name": tool_call["function"]["name"],
924
+ "arguments": json.dumps(tool_call["function"]["arguments"]) if isinstance(tool_call["function"].get("arguments"), (dict, list)) else tool_call["function"].get("arguments", "{}"),
925
+ "call_id": tool_call["id"], # Normalize "id" to "call_id"
816
926
  }
817
927
 
818
- # Append to updated_messages
819
- updated_messages.append(tool_result_msg)
820
-
821
- yield StreamChunk(
822
- type="custom_tool_status",
823
- status="function_call",
824
- content=f"Arguments for Calling {function_name}: {json.dumps(tool_call['function'].get('arguments', {}))}",
825
- source=f"custom_{function_name}",
826
- )
827
-
828
- yield StreamChunk(
829
- type="custom_tool_status",
830
- status="function_call_output",
831
- content=f"Results for Calling {function_name}: {result_str}",
832
- source=f"custom_{function_name}",
833
- )
834
-
835
- logger.info(f"Executed custom function {function_name}")
836
- yield StreamChunk(
837
- type="custom_tool_status",
838
- status="custom_tool_response",
839
- content=f"✅ [Custom Tool] {function_name} completed",
840
- source=f"custom_{function_name}",
841
- )
842
-
843
- # Then execute MCP tool calls and append results
928
+ # Execute custom tools using unified method
929
+ for tool_call in custom_tool_calls:
930
+ # Normalize Claude tool call format to unified format
931
+ normalized_call = normalize_tool_call(tool_call)
932
+
933
+ # Use unified execution method
934
+ async for chunk in self._execute_tool_with_logging(
935
+ normalized_call,
936
+ CUSTOM_TOOL_CONFIG,
937
+ updated_messages,
938
+ set(),
939
+ ):
940
+ yield chunk
941
+
942
+ # Execute MCP tools using unified method
844
943
  for tool_call in mcp_tool_calls:
845
- function_name = tool_call["function"]["name"]
944
+ # Normalize Claude tool call format to unified format
945
+ normalized_call = normalize_tool_call(tool_call)
946
+
947
+ # Use unified execution method
948
+ async for chunk in self._execute_tool_with_logging(
949
+ normalized_call,
950
+ MCP_TOOL_CONFIG,
951
+ updated_messages,
952
+ set(),
953
+ ):
954
+ yield chunk
846
955
 
847
- # Yield MCP tool call status
848
- yield StreamChunk(
849
- type="mcp_status",
850
- status="mcp_tool_called",
851
- content=f"🔧 [MCP Tool] Calling {function_name}...",
852
- source=f"mcp_{function_name}",
853
- )
854
-
855
- try:
856
- # Execute MCP function
857
- args_json = json.dumps(tool_call["function"]["arguments"]) if isinstance(tool_call["function"].get("arguments"), (dict, list)) else tool_call["function"].get("arguments", "{}")
858
- result_list = await self._execute_mcp_function_with_retry(function_name, args_json)
859
- if not result_list or (isinstance(result_list[0], str) and result_list[0].startswith("Error:")):
860
- logger.warning(f"MCP function {function_name} failed after retries: {result_list[0] if result_list else 'unknown error'}")
861
- continue
862
- result_str = result_list[0]
863
- result_obj = result_list[1] if len(result_list) > 1 else None
864
- except Exception as e:
865
- logger.error(f"Unexpected error in MCP function execution: {e}")
866
- continue
867
-
868
- # Build tool result message: { "role":"user", "content":[{ "type":"tool_result", "tool_use_id": tool_call["id"], "content": result_str }] }
869
- tool_result_msg = {
870
- "role": "user",
871
- "content": [
872
- {
873
- "type": "tool_result",
874
- "tool_use_id": tool_call["id"],
875
- "content": result_str,
876
- },
877
- ],
878
- }
879
-
880
- # Append to updated_messages
881
- updated_messages.append(tool_result_msg)
882
-
883
- yield StreamChunk(
884
- type="mcp_status",
885
- status="function_call",
886
- content=f"Arguments for Calling {function_name}: {json.dumps(tool_call['function'].get('arguments', {}))}",
887
- source=f"mcp_{function_name}",
888
- )
889
-
890
- # If result_obj might be structured, try to display summary
891
- result_display = None
892
- try:
893
- if hasattr(result_obj, "content") and result_obj.content:
894
- part = result_obj.content[0]
895
- if hasattr(part, "text"):
896
- result_display = str(part.text)
897
- except Exception:
898
- result_display = None
899
- if result_display:
900
- yield StreamChunk(
901
- type="mcp_status",
902
- status="function_call_output",
903
- content=f"Results for Calling {function_name}: {result_display}",
904
- source=f"mcp_{function_name}",
905
- )
906
- else:
907
- yield StreamChunk(
908
- type="mcp_status",
909
- status="function_call_output",
910
- content=f"Results for Calling {function_name}: {result_str}",
911
- source=f"mcp_{function_name}",
912
- )
913
-
914
- logger.info(f"Executed MCP function {function_name} (stdio/streamable-http)")
915
- yield StreamChunk(
916
- type="mcp_status",
917
- status="mcp_tool_response",
918
- content=f"✅ [MCP Tool] {function_name} completed",
919
- source=f"mcp_{function_name}",
920
- )
921
-
922
- # Trim updated_messages using base class method
923
956
  updated_messages = self._trim_message_history(updated_messages)
924
957
 
925
- # After processing all tool calls, recurse
926
958
  async for chunk in self._stream_with_custom_and_mcp_tools(updated_messages, tools, client, **kwargs):
927
959
  yield chunk
928
960
  return
@@ -1167,10 +1199,8 @@ class ClaudeBackend(CustomToolAndMCPBackend):
1167
1199
  content=f"\n⚠️ {user_message} ({error}); continuing without MCP tools\n",
1168
1200
  )
1169
1201
 
1170
- # Build non-MCP configuration and stream fallback
1171
1202
  fallback_params = dict(api_params)
1172
1203
 
1173
- # Remove any MCP tools from the tools list
1174
1204
  if "tools" in fallback_params and self._mcp_functions:
1175
1205
  mcp_names = set(self._mcp_functions.keys())
1176
1206
  non_mcp_tools = []
@@ -1195,7 +1225,7 @@ class ClaudeBackend(CustomToolAndMCPBackend):
1195
1225
  function_name: str,
1196
1226
  arguments_json: str,
1197
1227
  max_retries: int = 3,
1198
- ) -> List[str | Any]:
1228
+ ) -> Tuple[str, Any]:
1199
1229
  """Execute MCP function with Claude-specific formatting."""
1200
1230
  # Use parent class method which returns tuple
1201
1231
  result_str, result_obj = await super()._execute_mcp_function_with_retry(
@@ -1204,10 +1234,9 @@ class ClaudeBackend(CustomToolAndMCPBackend):
1204
1234
  max_retries,
1205
1235
  )
1206
1236
 
1207
- # Convert to list format expected by Claude streaming
1208
1237
  if result_str.startswith("Error:"):
1209
- return [result_str]
1210
- return [result_str, result_obj]
1238
+ return (result_str, {"error": result_str})
1239
+ return (result_str, result_obj)
1211
1240
 
1212
1241
  def create_tool_result_message(self, tool_call: Dict[str, Any], result_content: str) -> Dict[str, Any]:
1213
1242
  """Create tool result message in Claude's expected format."""