Task configuration:
{"task_id": "11", "task_name": "create_cylinder_with_holes", "description": "Create a cylinder with holes in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task11.FCStd", "cylinder_radius": 10, "cylinder_height": 20, "hole_radius": 2, "hole_count": 2}, "instruction_template": "The application is already open. Create a cylinder with radius ${cylinder_radius} and height ${cylinder_height}, and create ${hole_count} holes with radius ${hole_radius} evenly distributed on its side. These holes should be evenly spaced along the height of the cylinder, and the center of each hole should maintain the same horizontal distance from the cylinder's axis. The cylinder should be created using 'create an additive cylinder', and the holes should be created using 'create subtractive cylinder'. Finally, save the file to ${source_path}${filename}", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 240}, "preconditions": {"FreeCAD_running": true}, "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": "Save document key content detected", "is_key_step": true, "key_step_index": 2, "key_step_name": "Cylinder with holes created and saved successfully"}, "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 Cylinder with Holes Monitoring Hook Script
// Used to monitor FreeCAD's cylinder with holes creation operation and detect task completion
// After creating a cylinder with holes and saving the file, the test program listens for the save and checks if there is a cylinder with holes meeting the requirements in the corresponding document

(function() {
  // Script constant settings
  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 variable
  let funcFound = false;
  
  // Send event to the evaluation system
  function sendEvent(eventType, data = {}) {
      const payload = {
          event: eventType,
          ...data,
          timestamp: new Date().getTime()
      };
      send(payload);
  }
  
  // Find Document::saveToFile function
  function getFunction() {
      // Try to find directly through exported symbols
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);
      
      // If not found, report an error
      if (!FuncAddr) {
          sendEvent(ERROR, {
              error_type: FUNCTION_NOT_FOUND,
              message: `Cannot find ${ORIGIN_FUNCTION_NAME} function`
          });
          return null;
      }
      
      // Report function found
      funcFound = true;
      sendEvent(FUNCTION_FOUND, {
          address: FuncAddr.toString(),
          message: `Found ${ORIGIN_FUNCTION_NAME} function`
      });
      
      return FuncAddr;
  }
  
  // Initialize hook and execute immediately
  function initHook() {
      sendEvent(SCRIPT_INITIALIZED, {
          message: `${APP_NAME} ${FUNCTION_BEHAVIOR} monitoring script started`
      });
      
      // Find search function
      const funcAddr = getFunction();
      if (!funcAddr) {
          return;
      }
      
      // Install search function hook
      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 specified file
file_path = '${this.filename}'
doc = FreeCAD.open(file_path)

# Get active document
if doc is None:
    result = None
else:
    # Find cylinder and holes in the document
    main_cylinder = None
    holes = []
    
    # Check all objects, look for cylinder and holes
    for obj in doc.Objects:
        # Check if it's a solid object
        if hasattr(obj, "Shape"):
            # Check shape type
            if hasattr(obj.Shape, "ShapeType"):
                # For objects created using Part Design method, we need to check sub-objects
                if obj.TypeId == "PartDesign::Body":
                    # Prevent counting the same object multiple times
                    processed_objects = set()
                    
                    for subobj in obj.OutList:
                        if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "ShapeType"):
                            # Use TypeId to check if it's an additive cylinder
                            if subobj.TypeId == 'PartDesign::AdditiveCylinder':
                                # Use object ID as unique identifier
                                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
                                
                                # Skip if this object has been processed
                                if obj_id in processed_objects:
                                    continue
                                processed_objects.add(obj_id)
                                
                                if subobj.Shape.ShapeType == "Solid" and hasattr(subobj.Shape, "Volume"):
                                    # Check if it's a cylinder, read properties directly
                                    cylinder_radius = subobj.Radius.Value if hasattr(subobj, "Radius") else 0
                                    cylinder_height = subobj.Height.Value if hasattr(subobj, "Height") else 0
                                    
                                    # Get position information
                                    cylinder_position = None
                                    if hasattr(subobj, "Placement") and hasattr(subobj.Placement, "Base"):
                                        cylinder_position = {
                                            'x': subobj.Placement.Base.x,
                                            'y': subobj.Placement.Base.y,
                                            'z': subobj.Placement.Base.z
                                        }
                                    
                                    # If no direct properties, try calculating from shape
                                    if cylinder_radius == 0 or cylinder_height == 0:
                                        if len(subobj.Shape.Faces) == 3:  # Cylinder typically has three faces (two bases and one side)
                                            # Check for circular faces
                                            circular_faces = []
                                            cylindrical_face = None
                                            
                                            for face in subobj.Shape.Faces:
                                                if face.Surface.TypeId == 'Part::GeomCylinder':
                                                    cylindrical_face = face
                                                elif face.Surface.TypeId == 'Part::GeomPlane':
                                                    # Check if edges form a circle
                                                    if len(face.Edges) == 1 and face.Edges[0].Curve.TypeId == 'Part::GeomCircle':
                                                        circular_faces.append(face)
                                            
                                            # If found one cylindrical face and two circular faces, can calculate radius and height
                                            if cylindrical_face and len(circular_faces) == 2:
                                                if cylinder_radius == 0:
                                                    cylinder_radius = circular_faces[0].Edges[0].Curve.Radius.Value if hasattr(circular_faces[0].Edges[0].Curve.Radius, "Value") else circular_faces[0].Edges[0].Curve.Radius
                                                
                                                if cylinder_height == 0:
                                                    center1 = circular_faces[0].Surface.Position
                                                    center2 = circular_faces[1].Surface.Position
                                                    cylinder_height = math.sqrt(
                                                        (center1.x - center2.x)**2 + 
                                                        (center1.y - center2.y)**2 + 
                                                        (center1.z - center2.z)**2
                                                    )
                                    
                                    # Store cylinder information
                                    main_cylinder = {
                                        'radius': cylinder_radius,
                                        'height': cylinder_height,
                                        'position': cylinder_position
                                    }
                            
                            # Use TypeId to check if it's a subtractive cylinder (hole)
                            elif subobj.TypeId == 'PartDesign::SubtractiveCylinder':
                                # Use object ID as unique identifier
                                obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
                                
                                # Skip if this object has been processed
                                if obj_id in processed_objects:
                                    continue
                                processed_objects.add(obj_id)
                                
                                if subobj.Shape.ShapeType == "Solid" and hasattr(subobj.Shape, "Volume"):
                                    # Get radius directly from object
                                    hole_radius = subobj.Radius.Value if hasattr(subobj, "Radius") else 0
                                    
                                    # Get position information
                                    position = None
                                    if hasattr(subobj, "Placement") and hasattr(subobj.Placement, "Base"):
                                        position = {
                                            'x': subobj.Placement.Base.x,
                                            'y': subobj.Placement.Base.y,
                                            'z': subobj.Placement.Base.z
                                        }
                                    
                                    # Record all subtractive cylinders as holes, no need to check visibility
                                    holes.append({
                                        'radius': hole_radius,
                                        'position': position
                                    })
    
    # Return result - ensure returning pure numbers instead of values with units
    # Handle values that might have units
    def extract_value(val):
        if val is None:
            return None
        try:
            # If it's a string with unit (like "10.0 mm"), extract the numeric part
            if isinstance(val, str) and ' ' in val:
                return float(val.split()[0])
            # If it's a FreeCAD Quantity object, try converting to float
            return float(val)
        except:
            # If conversion fails, return original value
            return val
    
    # Extract cylinder center axis position
    cylinder_center_x = main_cylinder['position']['x'] if main_cylinder and main_cylinder['position'] else 0
    cylinder_center_y = main_cylinder['position']['y'] if main_cylinder and main_cylinder['position'] else 0
    
    # Prepare hole position information
    hole_positions = []
    for hole in holes:
        if hole['position']:
            hole_positions.append({
                'x': hole['position']['x'],
                'y': hole['position']['y'],
                'z': hole['position']['z']
            })
    
    # Sort hole positions - by Z coordinate
    sorted_hole_positions = sorted(hole_positions, key=lambda p: p['z']) if hole_positions else []
    
    result = {
        'cylinder_radius': extract_value(main_cylinder['radius']) if main_cylinder else None,
        'cylinder_height': extract_value(main_cylinder['height']) if main_cylinder else None,
        'hole_radius': extract_value(holes[0]['radius']) if holes else None,
        'hole_count': len(holes),
        'cylinder_position': main_cylinder['position'] if main_cylinder else None,
        'hole_positions': sorted_hole_positions
    }
                    `
                    sendEvent(FUNCTION_KEY_WORD_DETECTED, {
                        message: `Detected ${FUNCTION_BEHAVIOR} operation`,
                        filename: this.filename,
                        code: pythonCode
                    });
                }
              } 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 Cylinder with Holes Event Handler
Handles events generated by the hook script and updates evaluation metrics
"""

from typing import Dict, Any, Optional, List
import math

# 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
CYLINDER_RADIUS = "cylinder_radius"
CYLINDER_HEIGHT = "cylinder_height"
HOLE_RADIUS = "hole_radius"
HOLE_COUNT = "hole_count"

def execute_python_code(code: str, logger: Any) -> Dict[str, Any]:
    """
    Execute Python code and return results
    
    Args:
        code: Python code to execute
        logger: Logger instance
        
    Returns:
        Dict[str, Any]: Execution results
    """
    try:
        # Create a new namespace to execute the code
        namespace = {}
        exec(code, namespace)
        result = namespace.get('result', None)
        
        if result is None:
            logger.warning("Cylinder with holes object not found")
            return None
            
        # Validate result format
        required_keys = [CYLINDER_RADIUS, CYLINDER_HEIGHT, HOLE_RADIUS, HOLE_COUNT]
        if not all(key in result for key in required_keys):
            logger.error(f"Result 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 hook script
    
    Args:
        message: Frida message object
        logger: Logger instance
        task_parameter: Task parameters
        
    Returns:
        Optional[List[Dict[str, Any]]]: List of status updates, 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 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 results
                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 path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if cylinder with holes parameters match expectations
                        expected_cylinder_radius = task_parameter.get(CYLINDER_RADIUS, 10)
                        expected_cylinder_height = task_parameter.get(CYLINDER_HEIGHT, 20)
                        expected_hole_radius = task_parameter.get(HOLE_RADIUS, 2)
                        expected_hole_count = task_parameter.get(HOLE_COUNT, 2)

                        actual_cylinder_radius = result[CYLINDER_RADIUS]
                        actual_cylinder_height = result[CYLINDER_HEIGHT]
                        actual_hole_radius = result[HOLE_RADIUS]
                        actual_hole_count = result[HOLE_COUNT]

                        # Log key parameters (these are necessary information, not debugging)
                        logger.info(f"Task parameter check: Expected cylinder radius {expected_cylinder_radius}, Expected cylinder height {expected_cylinder_height}, " +
                                    f"Expected hole radius {expected_hole_radius}, Expected hole count {expected_hole_count}")
                        logger.info(f"Actual parameter check: Actual cylinder radius {actual_cylinder_radius}, Actual cylinder height {actual_cylinder_height}, " +
                                    f"Actual hole radius {actual_hole_radius}, Actual hole count {actual_hole_count}")

                        # Convert values that might have units to float
                        try:
                            # Try to get numeric part (handle possible units)
                            actual_cylinder_radius_value = float(str(actual_cylinder_radius).split()[0]) if isinstance(actual_cylinder_radius, str) else float(actual_cylinder_radius)
                            actual_cylinder_height_value = float(str(actual_cylinder_height).split()[0]) if isinstance(actual_cylinder_height, str) else float(actual_cylinder_height)
                            actual_hole_radius_value = float(str(actual_hole_radius).split()[0]) if isinstance(actual_hole_radius, str) else float(actual_hole_radius)
                            
                            # Allow small error margin (0.01%)
                            cylinder_radius_error = abs((actual_cylinder_radius_value - expected_cylinder_radius) / expected_cylinder_radius) <= 0.0001
                            cylinder_height_error = abs((actual_cylinder_height_value - expected_cylinder_height) / expected_cylinder_height) <= 0.0001
                            hole_radius_error = abs((actual_hole_radius_value - expected_hole_radius) / expected_hole_radius) <= 0.0001
                            hole_count_correct = actual_hole_count == expected_hole_count
                        except Exception as e:
                            logger.error(f"Parameter comparison error: {str(e)}")
                            cylinder_radius_error = False
                            cylinder_height_error = False
                            hole_radius_error = False
                            hole_count_correct = False
                        
                        # Check if holes are positioned correctly
                        position_correct = True
                        
                        # Get hole position information
                        hole_positions = result.get('hole_positions', [])
                        cylinder_position = result.get('cylinder_position', {'x': 0, 'y': 0, 'z': 0})
                        
                        if hole_positions and len(hole_positions) >= 2 and cylinder_position:
                            # Check 1: Are holes evenly distributed along height
                            cylinder_center_x = cylinder_position.get('x', 0)
                            cylinder_center_y = cylinder_position.get('y', 0)
                            
                            # If there are multiple holes, check if they are evenly distributed along height
                            if len(hole_positions) > 1:
                                # Sort holes by z coordinate
                                sorted_holes = sorted(hole_positions, key=lambda p: p.get('z', 0))
                                
                                # Calculate height differences between adjacent holes
                                height_diffs = []
                                for i in range(1, len(sorted_holes)):
                                    height_diff = sorted_holes[i].get('z', 0) - sorted_holes[i-1].get('z', 0)
                                    height_diffs.append(height_diff)
                                
                                # Check if height differences are uniform (0.1% tolerance)
                                if height_diffs and len(height_diffs) > 0:
                                    avg_height_diff = sum(height_diffs) / len(height_diffs)
                                    if avg_height_diff == 0:
                                        position_correct = False
                                    else:
                                        height_uniformity = all(
                                            abs((diff - avg_height_diff) / avg_height_diff) <= 0.001 
                                            for diff in height_diffs
                                        )
                                        if not height_uniformity:
                                            logger.warning("Holes are not evenly distributed along height")
                                            position_correct = False
                            
                            # Check 2: Are holes maintaining same horizontal distance from cylinder axis
                            horizontal_distances = []
                            for hole in hole_positions:
                                hole_x = hole.get('x', 0)
                                hole_y = hole.get('y', 0)
                                
                                # Calculate horizontal distance (to cylinder center axis)
                                horizontal_distance = math.sqrt(
                                    (hole_x - cylinder_center_x)**2 + 
                                    (hole_y - cylinder_center_y)**2
                                )
                                horizontal_distances.append(horizontal_distance)
                            
                            # Check if all holes have same horizontal distance (0.1% tolerance)
                            if horizontal_distances and len(horizontal_distances) > 0:
                                avg_distance = sum(horizontal_distances) / len(horizontal_distances)
                                if avg_distance > 0:
                                    distance_uniformity = all(
                                        abs((dist - avg_distance) / avg_distance) <= 0.001 
                                        for dist in horizontal_distances
                                    )
                                    
                                    if not distance_uniformity:
                                        logger.warning("Holes are not maintaining consistent distance from cylinder center axis")
                                        position_correct = False
                                else:
                                    logger.warning("Hole horizontal distance is 0, cannot verify distribution")
                                    position_correct = False
                            if not position_correct:
                                logger.info(f"Hole position distribution validation failed: {hole_positions}")
                        elif expected_hole_count > 1:
                            logger.warning("Could not get enough hole position information for distribution validation")
                            position_correct = False
                        
                        # Check all conditions
                        if cylinder_radius_error and cylinder_height_error and hole_radius_error and hole_count_correct and position_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Cylinder with holes created and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created and saved a cylinder with holes meeting requirements'
                            })
                        else:
                            failure_reasons = []
                            if not cylinder_radius_error:
                                failure_reasons.append("Incorrect cylinder radius")
                            if not cylinder_height_error:
                                failure_reasons.append("Incorrect cylinder height")
                            if not hole_radius_error:
                                failure_reasons.append("Incorrect hole radius")
                            if not hole_count_correct:
                                failure_reasons.append("Incorrect hole count")
                            if not position_correct:
                                failure_reasons.append("Hole distribution does not meet requirements")
                            
                            logger.warning(f"Parameter validation failed: {', '.join(failure_reasons)}")
                
            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