Task configuration:
{"task_id": "14", "task_name": "create_cone_with_sphere_cut", "description": "Create a cone with a hemispherical cutout in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task14.FCStd", "cone_radius1": 25.0, "cone_radius2": 10.0, "cone_height": 40.0, "sphere_radius": 8.0, "sphere_position_x": 0.0, "sphere_position_y": 0.0, "sphere_position_z": 40.0}, "instruction_template": "The application is already open. In FreeCAD, create a cone with a bottom radius of ${cone_radius1}mm, a top radius of ${cone_radius2}mm, and a height of ${cone_height}mm. Then, create a hemispherical cutout at the center of its top surface. The radius of the cutting sphere is ${sphere_radius}mm, and its center should be positioned at the cone's top surface center (${sphere_position_x}, ${sphere_position_y}, ${sphere_position_z})mm. The cone should be created using the 'create an additive cone' method (typeid), and the hemispherical cutout should be created using the 'create subtractive sphere' method (typeid). Do not create the shapes separately and use Boolean subtraction or cut. Finally, save the file to ${source_path}${filename}.", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 300, "evaluator_type": "HookManager"}, "preconditions": {"freecad_running": false}, "total_key_steps": 2, "events": {"script_initialized": {"description": "Hook script initialized", "is_key_step": false}, "function_found": {"description": "Save document function found", "is_key_step": false}, "function_called": {"description": "Save document function called", "is_key_step": true, "key_step_index": 1, "key_step_name": "Save document"}, "function_key_word_detected": {"description": "Detected key content for saving document", "is_key_step": true, "key_step_index": 2, "key_step_name": "Successfully created a cone with a hemispherical cutout and saved"}, "error": {"description": "Error event", "is_key_step": false}}, "mcp_servers": [{"command": "python3", "args": ["/home/agent/.local/share/FreeCAD/Mod/freecad_mcp/src/freecad_bridge.py"]}]}

Hook script:
// FreeCAD Cone with Hemispherical Cutout Monitoring Hook Script
// Monitors the creation of a cone with a hemispherical cutout in FreeCAD and checks task completion
// After creating the cone with a hemispherical cutout, save the file. The test program listens for the save event and queries the corresponding document to verify the presence of the required cone with a hemispherical cutout.

(function() {
  // Script constants
  const FUNCTION_NAME = "_ZNK3App8Document10saveToFileEPKc"
  const ORIGIN_FUNCTION_NAME = "Document::saveToFile"
  const FUNCTION_BEHAVIOR = "Save Document"

  const SCRIPT_INITIALIZED = "script_initialized"
  const FUNCTION_NOT_FOUND = "function_not_found"
  const FUNCTION_FOUND = "function_found"
  const FUNCTION_CALLED = "function_called"
  const FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
  const ERROR = "error"
  const HOOK_INSTALLED = "hook_installed"

  const APP_NAME = "FreeCAD"

  // Global variables
  let funcFound = false;

  // Send events to the evaluation system
  function sendEvent(eventType, data = {}) {
      const payload = {
          event: eventType,
          ...data,
          timestamp: new Date().getTime()
      };
      send(payload);
  }

  // Locate the Document::saveToFile function
  function getFunction() {
      // Attempt to locate via exported symbols
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);

      // If not found, report an error
      if (!FuncAddr) {
          sendEvent(ERROR, {
              error_type: FUNCTION_NOT_FOUND,
              message: `Unable to locate ${ORIGIN_FUNCTION_NAME} function`
          });
          return null;
      }

      // Report function found
      funcFound = true;
      sendEvent(FUNCTION_FOUND, {
          address: FuncAddr.toString(),
          message: `Located ${ORIGIN_FUNCTION_NAME} function`
      });

      return FuncAddr;
  }

  // Initialize the hook and execute immediately
  function initHook() {
      sendEvent(SCRIPT_INITIALIZED, {
          message: `${APP_NAME} ${FUNCTION_BEHAVIOR} monitoring script started`
      });

      // Locate the target function
      const funcAddr = getFunction();
      if (!funcAddr) {
          return;
      }

      // Install the hook for the target function
      Interceptor.attach(funcAddr, {
          onEnter: function(args) {
              try {
                  sendEvent(FUNCTION_CALLED, {
                      message: `Intercepted ${FUNCTION_BEHAVIOR} function call`
                  });
                  this.filename = args[1].readCString();
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          },

          onLeave: function(retval) {
              try {
                if (retval) {
                    const pythonCode = `
import freecad
import FreeCAD
import Part
import math

# Open the specified file
file_path = '${this.filename}'
doc = FreeCAD.open(file_path)

# Get the active document
if doc is None:
    result = None
else:
    # Locate the cone and spherical cutout
    cone = None
    sphere_cut = None
    processed_objects = set()
    # Check all objects to find the cone and spherical cutout
    for subobj in doc.Objects:
        if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "ShapeType"):
            # Check if it is an additive cone using TypeId
            if subobj.TypeId == 'PartDesign::AdditiveCone':
                # Use object ID as a unique identifier
                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name

                # Skip if this object has already been processed
                if obj_id in processed_objects:
                    continue
                processed_objects.add(obj_id)

                if subobj.Shape.ShapeType == "Solid":
                    # Check if it is a cone and directly read properties
                    cone_radius1 = subobj.Radius1.Value if hasattr(subobj, "Radius1") else 0
                    cone_radius2 = subobj.Radius2.Value if hasattr(subobj, "Radius2") else 0
                    cone_height = subobj.Height.Value if hasattr(subobj, "Height") else 0

                    # Store cone information
                    cone = {
                        'radius1': cone_radius1,
                        'radius2': cone_radius2,
                        'height': cone_height
                    }

            # Check if it is a subtractive sphere (spherical cutout) using TypeId
            elif subobj.TypeId == 'PartDesign::SubtractiveSphere':
                # Use object ID as a unique identifier
                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name

                # Skip if this object has already been processed
                if obj_id in processed_objects:
                    continue
                processed_objects.add(obj_id)

                if subobj.Shape.ShapeType == "Solid":
                    # Directly retrieve sphere properties
                    sphere_radius = subobj.Radius.Value if hasattr(subobj, "Radius") else 0

                    # Retrieve sphere position
                    sphere_position_x = 0.0
                    sphere_position_y = 0.0
                    sphere_position_z = 0.0

                    if hasattr(subobj, "Placement") and hasattr(subobj.Placement, "Base"):
                        sphere_position_x = subobj.Placement.Base.x
                        sphere_position_y = subobj.Placement.Base.y
                        sphere_position_z = subobj.Placement.Base.z

                    # Store spherical cutout information
                    sphere_cut = {
                        'radius': sphere_radius,
                        'position_x': sphere_position_x,
                        'position_y': sphere_position_y,
                        'position_z': sphere_position_z
                    }

    # Return the result - ensure pure numeric values without units
    # Handle values that may include units
    def extract_value(val):
        if val is None:
            return None
        try:
            # If the value is a string with units (e.g., "10.0 mm"), extract the numeric part
            if isinstance(val, str) and ' ' in val:
                return float(val.split()[0])
            # If the value is a FreeCAD Quantity object, attempt to convert to float
            return float(val)
        except:
            # If conversion fails, return the original value
            return val

    result = {
        'cone_radius1': extract_value(cone['radius1']) if cone else None,
        'cone_radius2': extract_value(cone['radius2']) if cone else None,
        'cone_height': extract_value(cone['height']) if cone else None,
        'sphere_radius': extract_value(sphere_cut['radius']) if sphere_cut else None,
        'sphere_position_x': extract_value(sphere_cut['position_x']) if sphere_cut else None,
        'sphere_position_y': extract_value(sphere_cut['position_y']) if sphere_cut else None,
        'sphere_position_z': extract_value(sphere_cut['position_z']) if sphere_cut else None,
        'has_sphere_cut': sphere_cut is not None
    }
                    `
                    sendEvent(FUNCTION_KEY_WORD_DETECTED, {
                        message: `Detected ${FUNCTION_BEHAVIOR} operation`,
                        filename: this.filename,
                        code: pythonCode
                    });
                }
                // Detect keywords
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          }
      });

      sendEvent(HOOK_INSTALLED, {
          message: `Hook installed, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }

  // Execute hook initialization immediately
  initHook();
})();


Evaluation handler:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
FreeCAD Cone with Hemispherical Cutout Event Handler
Responsible for processing events generated by the hook script and updating evaluation metrics
"""

from typing import Dict, Any, Optional, List

# Event type constants
SCRIPT_INITIALIZED = "script_initialized"
FUNCTION_NOT_FOUND = "function_not_found"
FUNCTION_FOUND = "function_found"
FUNCTION_CALLED = "function_called"
FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
ERROR = "error"
HOOK_INSTALLED = "hook_installed"

# Keyword-related constants
CONE_RADIUS1 = "cone_radius1"
CONE_RADIUS2 = "cone_radius2"
CONE_HEIGHT = "cone_height"
SPHERE_RADIUS = "sphere_radius"
SPHERE_POSITION_X = "sphere_position_x"
SPHERE_POSITION_Y = "sphere_position_y"
SPHERE_POSITION_Z = "sphere_position_z"
HAS_SPHERE_CUT = "has_sphere_cut"

def execute_python_code(code: str, logger: Any) -> Dict[str, Any]:
    """
    Execute Python code and return the result
    
    Args:
        code: Python code to execute
        logger: Logger instance
        
    Returns:
        Dict[str, Any]: Execution result
    """
    try:
        # Create a new namespace to execute the code
        namespace = {}
        exec(code, namespace)
        result = namespace.get('result', None)
        
        if result is None:
            logger.warning("Cone with hemispherical cutout object not found")
            return None
            
        # Validate result format
        required_keys = [
            CONE_RADIUS1, CONE_RADIUS2, CONE_HEIGHT, 
            SPHERE_RADIUS, SPHERE_POSITION_X, SPHERE_POSITION_Y, 
            SPHERE_POSITION_Z, HAS_SPHERE_CUT
        ]
        if not all(key in result for key in required_keys):
            logger.error(f"Result is missing required keys: {required_keys}")
            return None
            
        return result
    except Exception as e:
        logger.error(f"Error executing Python code: {str(e)}")
        return None

def message_handler(message: Dict[str, Any], logger: Any, task_parameter: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
    """
    Handle messages received from the hook script
    
    Args:
        message: Frida message object
        logger: Logger instance
        task_parameter: Task parameters
        
    Returns:
        Optional[List[Dict[str, Any]]]: List of status updates, or None if no updates
    """
    updates = []
    
    if message.get('type') == 'send' and 'payload' in message:
        payload = message['payload']
        
        if 'event' in payload:
            event_type = payload['event']
            logger.debug(f"Received event: {event_type}")
            
            if event_type == SCRIPT_INITIALIZED:
                logger.info(f"Hook script initialized: {payload.get('message', '')}")
                
            elif event_type == FUNCTION_FOUND:
                logger.info(f"Function found: {payload.get('address', '')}")
                
            elif event_type == FUNCTION_CALLED:
                logger.info(f"Function called: {payload.get('message', '')}")
                # Update the first key step status
                updates.append({
                    'status': 'key_step',
                    'index': 1,
                    'name': 'Save document'
                })
                
            elif event_type == FUNCTION_KEY_WORD_DETECTED:
                # Execute Python code and get the result
                code = payload.get('code', '')
                filename = payload.get('filename', '')
                expected_path = task_parameter.get("source_path", "") + task_parameter.get("filename", "")
                logger.info(f"Keyword detected, document path: {filename}, expected document path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if the cone with spherical cutout parameters meet expectations
                        expected_cone_radius1 = task_parameter.get(CONE_RADIUS1, 25.0)
                        expected_cone_radius2 = task_parameter.get(CONE_RADIUS2, 10.0)
                        expected_cone_height = task_parameter.get(CONE_HEIGHT, 40.0)
                        expected_sphere_radius = task_parameter.get(SPHERE_RADIUS, 15.0)
                        expected_sphere_position_x = task_parameter.get(SPHERE_POSITION_X, 0.0)
                        expected_sphere_position_y = task_parameter.get(SPHERE_POSITION_Y, 0.0)
                        expected_sphere_position_z = task_parameter.get(SPHERE_POSITION_Z, 25.0)

                        actual_cone_radius1 = result[CONE_RADIUS1]
                        actual_cone_radius2 = result[CONE_RADIUS2]
                        actual_cone_height = result[CONE_HEIGHT]
                        actual_sphere_radius = result[SPHERE_RADIUS]
                        actual_sphere_position_x = result[SPHERE_POSITION_X]
                        actual_sphere_position_y = result[SPHERE_POSITION_Y]
                        actual_sphere_position_z = result[SPHERE_POSITION_Z]
                        actual_has_sphere_cut = result[HAS_SPHERE_CUT]

                        # Log key parameters
                        logger.info(f"Task parameter check: Expected cone bottom radius={expected_cone_radius1}, top radius={expected_cone_radius2}, height={expected_cone_height}")
                        logger.info(f"Expected spherical cutout: radius={expected_sphere_radius}, position=({expected_sphere_position_x}, {expected_sphere_position_y}, {expected_sphere_position_z})")
                        logger.info(f"Actual parameters: Actual cone bottom radius={actual_cone_radius1}, top radius={actual_cone_radius2}, height={actual_cone_height}")
                        logger.info(f"Actual spherical cutout: radius={actual_sphere_radius}, position=({actual_sphere_position_x}, {actual_sphere_position_y}, {actual_sphere_position_z}), " +
                                   f"Has spherical cutout: {actual_has_sphere_cut}")

                        try:
                            # Attempt to extract numerical values (handle possible units)
                            actual_cone_radius1_value = float(str(actual_cone_radius1).split()[0]) if isinstance(actual_cone_radius1, str) else float(actual_cone_radius1)
                            actual_cone_radius2_value = float(str(actual_cone_radius2).split()[0]) if isinstance(actual_cone_radius2, str) else float(actual_cone_radius2)
                            actual_cone_height_value = float(str(actual_cone_height).split()[0]) if isinstance(actual_cone_height, str) else float(actual_cone_height)
                            
                            actual_sphere_radius_value = float(str(actual_sphere_radius).split()[0]) if isinstance(actual_sphere_radius, str) else float(actual_sphere_radius)
                            actual_sphere_position_x_value = float(str(actual_sphere_position_x).split()[0]) if isinstance(actual_sphere_position_x, str) else float(actual_sphere_position_x)
                            actual_sphere_position_y_value = float(str(actual_sphere_position_y).split()[0]) if isinstance(actual_sphere_position_y, str) else float(actual_sphere_position_y)
                            actual_sphere_position_z_value = float(str(actual_sphere_position_z).split()[0]) if isinstance(actual_sphere_position_z, str) else float(actual_sphere_position_z)
                            
                            # Allow a certain margin of error (0.01%)
                            cone_radius1_error = abs((actual_cone_radius1_value - expected_cone_radius1) / expected_cone_radius1) <= 0.0001
                            cone_radius2_error = abs((actual_cone_radius2_value - expected_cone_radius2) / expected_cone_radius2) <= 0.0001
                            cone_height_error = abs((actual_cone_height_value - expected_cone_height) / expected_cone_height) <= 0.0001
                            
                            sphere_radius_error = abs((actual_sphere_radius_value - expected_sphere_radius) / expected_sphere_radius) <= 0.0001
                            # Position coordinates allow a certain margin of error (absolute error 1.0mm)
                            sphere_position_x_error = abs(actual_sphere_position_x_value - expected_sphere_position_x) <= 1.0
                            sphere_position_y_error = abs(actual_sphere_position_y_value - expected_sphere_position_y) <= 1.0
                            sphere_position_z_error = abs(actual_sphere_position_z_value - expected_sphere_position_z) <= 1.0
                            
                            has_sphere_cut_correct = actual_has_sphere_cut
                        except Exception as e:
                            logger.error(f"Error comparing parameters: {str(e)}")
                            cone_radius1_error = False
                            cone_radius2_error = False
                            cone_height_error = False
                            sphere_radius_error = False
                            sphere_position_x_error = False
                            sphere_position_y_error = False
                            sphere_position_z_error = False
                            has_sphere_cut_correct = False
                        
                        cone_correct = cone_radius1_error and cone_radius2_error and cone_height_error
                        sphere_correct = (sphere_radius_error and 
                                         sphere_position_x_error and 
                                         sphere_position_y_error and
                                         sphere_position_z_error and
                                         has_sphere_cut_correct)
                        
                        if cone_correct and sphere_correct:
                            # Update the second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Successfully created and saved cone with hemispherical cutout'
                            })
                            
                            # Task successfully completed
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created and saved cone with hemispherical cutout that meets requirements'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Cone correct: {cone_correct}, " +
                                          f"Spherical cutout correct: {sphere_correct}")
                
            elif event_type == ERROR:
                error_type = payload.get("error_type", "unknown")
                error_message = payload.get("message", "Unknown error")
                
                logger.error(f"Hook script error ({error_type}): {error_message}")
                
                # Record error event
                updates.append({
                    'status': 'error',
                    'type': error_type,
                    'message': error_message
                })
                
    elif message.get('type') == 'error':
        logger.error(f"Hook script error: {message.get('stack', '')}")
        
        # Record error event
        updates.append({
            'status': 'error',
            'type': 'script_error',
            'message': message.get('stack', 'Unknown error')
        })
    
    return updates if updates else None