Task configuration:
{"task_id": "19", "task_name": "create_pyramid_with_mirror", "description": "Create a pyramid with mirror feature in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task19.FCStd", "base_length": 50.0, "base_width": 50.0, "pyramid_height": 30.0, "mirror_plane": "XZ"}, "instruction_template": "The application is already open. Create a pyramid in FreeCAD with a rectangular base that has length ${base_length}mm and width ${base_width}mm, and a height of ${pyramid_height}mm. The base should be positioned on the XZ plane. Then add a mirror feature to this pyramid using the ${mirror_plane} plane. The pyramid can be created using 'create a addtive wedge'. The mirror feature should be created using 'apply a pattern -> mirror'. 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": "Document save function found", "is_key_step": false}, "function_called": {"description": "Document save function called", "is_key_step": true, "key_step_index": 1, "key_step_name": "Document saved"}, "function_key_word_detected": {"description": "Detected key content in saved document", "is_key_step": true, "key_step_index": 2, "key_step_name": "Created pyramid with mirror feature 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 Pyramid with Mirror Feature Monitoring Hook Script
// Used to monitor FreeCAD operations for creating a pyramid with mirror feature and detect task completion
// After creating a pyramid with mirror feature and saving the file, the test program detects the save operation and checks if the document contains a pyramid with mirror feature that meets the requirements

(function() {
  // Script constants setup
  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);
  }
  
  // Find Document::saveToFile function
  function getFunction() {
      // Try to find directly through export symbols
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);
      
      // If not found, report error
      if (!FuncAddr) {
          sendEvent(ERROR, {
              error_type: FUNCTION_NOT_FOUND,
              message: `Could not find ${ORIGIN_FUNCTION_NAME} function`
          });
          return null;
      }
      
      // 报告找到函数
      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 target function
      const funcAddr = getFunction();
      if (!funcAddr) {
          return;
      }
      
      // Install 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 pyramid and mirror feature
    pyramid = None
    mirror_feature = None
    has_mirror = False
    mirror_plane = ""
    base_length = 0.0
    base_width = 0.0
    pyramid_height = 0.0
    processed_objects = set()
    
    # Check all objects, looking for pyramid and mirror feature
    for subobj in doc.Objects:
        if 'Pyramid' in subobj.TypeId or 'AdditivePyramid' in subobj.TypeId:
            obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
            
            # If this object has already been processed, skip it
            if obj_id in processed_objects:
                continue
            processed_objects.add(obj_id)
            
            # Try to get pyramid parameters
            if hasattr(subobj, "Length"):
                base_length = subobj.Length.Value
            if hasattr(subobj, "Width"):
                base_width = subobj.Width.Value
            if hasattr(subobj, "Height"):
                pyramid_height = subobj.Height.Value
            
            pyramid = subobj
        
        # Check if there's a mirror feature
        elif 'Mirror' in subobj.TypeId:
            # Use object ID as a unique identifier
            obj_id = subobj.ID if hasattr(subobj, "ID") else subobj.Name
            
            # If this object has already been processed, skip it
            if obj_id in processed_objects:
                continue
            processed_objects.add(obj_id)
            
            # Mark that mirror feature exists
            has_mirror = True
            mirror_feature = subobj
            
            # Try to get mirror plane
            if hasattr(subobj, "MirrorPlane"):
                # Check mirror plane parameters
                mirror_plane_obj = subobj.MirrorPlane
                
                # Try to analyze mirror plane type
                if hasattr(mirror_plane_obj, "Axis"):
                    axis = mirror_plane_obj.Axis
                    # Determine plane based on axis
                    if abs(axis.x) > 0.9 and abs(axis.y) < 0.1 and abs(axis.z) < 0.1:
                        mirror_plane = "YZ"  # X axis perpendicular to YZ plane
                    elif abs(axis.y) > 0.9 and abs(axis.x) < 0.1 and abs(axis.z) < 0.1:
                        mirror_plane = "XZ"  # Y axis perpendicular to XZ plane
                    elif abs(axis.z) > 0.9 and abs(axis.x) < 0.1 and abs(axis.y) < 0.1:
                        mirror_plane = "XY"  # Z axis perpendicular to XY plane
    
    # If no pyramid found, try to identify by shape characteristics
    if not pyramid:
        # Iterate through all objects in the document
        for obj in doc.Objects:
            if hasattr(obj, "TypeId") and ("Wedge" in obj.TypeId or "Part::Wedge" in obj.TypeId):
                pyramid = obj
                if hasattr(obj, "Xmin") and hasattr(obj, "Xmax") and hasattr(obj, "Ymin") and hasattr(obj, "Ymax") and hasattr(obj, "Zmin") and hasattr(obj, "Zmax"):
                    base_length = abs(obj.Xmax - obj.Xmin)
                    base_width = abs(obj.Ymax - obj.Ymin)
                    pyramid_height = abs(obj.Zmax - obj.Zmin)
                continue
            
            # If not a direct wedge object, check vertex and face count
            if hasattr(obj.Shape, "Vertexes") and (len(obj.Shape.Vertexes) == 5 or len(obj.Shape.Vertexes) == 6):
                # A pyramid should have 5 faces (quadrangular pyramid) or 6 faces (wedge)
                if hasattr(obj.Shape, "Faces") and (len(obj.Shape.Faces) == 5 or len(obj.Shape.Faces) == 6):
                    pyramid = obj
                    
                    # Try to calculate dimensions
                    if hasattr(obj.Shape, "BoundBox"):
                        bbox = obj.Shape.BoundBox
                        # For wedges on XZ plane, X and Z are base dimensions
                        base_length = bbox.XLength
                        base_width = bbox.ZLength  # For wedges on XZ plane, Z is width
                        pyramid_height = bbox.YLength  # Y is height
    
    # If no mirror feature found, try to determine by checking symmetry
    if not has_mirror:
        # Check if there are possibly symmetrically placed similar objects in the document
        all_solids = []
        for obj in doc.Objects:
            if hasattr(obj, "Shape") and obj.Shape.ShapeType == "Solid":
                all_solids.append(obj)
        
        # If there are an even number of similar objects, a mirror might exist
        if len(all_solids) >= 2 and len(all_solids) % 2 == 0:
            # Try to check the relationship between these objects
            has_mirror = True
            
            # Assume the task requires XZ plane mirroring
            mirror_plane = "XZ"  # Since we know the base is on the XZ plane, the mirror plane is also XZ
            
            # Verify by checking the distribution of objects' positions
            if len(all_solids) >= 2:
                obj1 = all_solids[0]
                obj2 = all_solids[1]
                
                if hasattr(obj1.Shape, "CenterOfMass") and hasattr(obj2.Shape, "CenterOfMass"):
                    com1 = obj1.Shape.CenterOfMass
                    com2 = obj2.Shape.CenterOfMass
                    
                    # Infer mirror plane based on relative position of centers of mass
                    dx = abs(com1.x - com2.x)
                    dy = abs(com1.y - com2.y)
                    dz = abs(com1.z - com2.z)
                    
                    # For pyramids created as wedges, determine mirror plane
                    if dx < 0.1 and dz < 0.1 and dy > 0:
                        # If differences in X and Z directions are small, but Y is significant, it's XZ plane mirroring
                        mirror_plane = "XZ"  # Mirrored along Y axis
                    elif dy < 0.1 and dz < 0.1 and dx > 0:
                        mirror_plane = "YZ"  # Mirrored along X axis
                    elif dx < 0.1 and dy < 0.1 and dz > 0:
                        mirror_plane = "XY"  # Mirrored along Z axis
    
    # Return results - ensure pure numbers are returned 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 units (like "10.0 mm"), extract the numerical part
            if isinstance(val, str) and ' ' in val:
                return float(val.split()[0])
            # If it's a FreeCAD Quantity object, try to convert to float
            return float(val)
        except:
            # If conversion fails, return the original value
            return val
    
    # Print debug information for troubleshooting
    print("Debug info: Measured pyramid dimensions - Length:", base_length, "Width:", base_width, "Height:", pyramid_height)
    print("Debug info: Mirror feature - Exists:", has_mirror, "Mirror plane:", mirror_plane)
    
    # Because wedge-created pyramids have base on XZ plane, adjust dimension parameter mapping
    if pyramid and hasattr(pyramid.Shape, "BoundBox"):
        bbox = pyramid.Shape.BoundBox
        # Adjust parameter interpretation
        result = {
            'base_length': extract_value(base_length),
            'base_width': extract_value(base_width),
            'pyramid_height': extract_value(pyramid_height),
            'mirror_plane': "XZ",  # Fixed to XZ plane because the task requires base on XZ plane
            'has_mirror': has_mirror
        }
    else:
        result = {
            'base_length': extract_value(base_length),
            'base_width': extract_value(base_width),
            'pyramid_height': extract_value(pyramid_height),
            'mirror_plane': mirror_plane,
            'has_mirror': has_mirror
        }
                    `
                    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 installation complete, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }
  
  // Execute hook initialization immediately
  initHook();
})();


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

"""
FreeCAD Pyramid with Mirror Feature 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
BASE_LENGTH = "base_length"
BASE_WIDTH = "base_width"
PYRAMID_HEIGHT = "pyramid_height"
MIRROR_PLANE = "mirror_plane"
HAS_MIRROR = "has_mirror"

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 for recording events
        
    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("Pyramid with mirror feature object not found")
            return None
            
        # Validate result format
        required_keys = [
            BASE_LENGTH, BASE_WIDTH, PYRAMID_HEIGHT, MIRROR_PLANE, HAS_MIRROR
        ]
        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]]]:
    """
    Process messages received from the hook script
    
    Args:
        message: Frida message object
        logger: Logger for recording events
        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 first key step status
                updates.append({
                    'status': 'key_step',
                    'index': 1,
                    'name': 'Document saved'
                })
                
            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"Key content detected, document path: {filename}, expected document path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if pyramid parameters match expectations
                        expected_base_length = task_parameter.get(BASE_LENGTH, 50.0)
                        expected_base_width = task_parameter.get(BASE_WIDTH, 50.0)
                        expected_pyramid_height = task_parameter.get(PYRAMID_HEIGHT, 30.0)
                        expected_mirror_plane = task_parameter.get(MIRROR_PLANE, "XZ")

                        actual_base_length = result[BASE_LENGTH]
                        actual_base_width = result[BASE_WIDTH]
                        actual_pyramid_height = result[PYRAMID_HEIGHT]
                        actual_mirror_plane = result[MIRROR_PLANE]
                        actual_has_mirror = result[HAS_MIRROR]

                        # Log key parameters
                        logger.info(f"Task parameter check: Expected pyramid base length={expected_base_length}, base width={expected_base_width}, height={expected_pyramid_height}")
                        logger.info(f"Expected mirror plane: {expected_mirror_plane}")
                        logger.info(f"Actual parameters: Actual pyramid base length={actual_base_length}, base width={actual_base_width}, height={actual_pyramid_height}")
                        logger.info(f"Actual mirror plane: {actual_mirror_plane}, has mirror: {actual_has_mirror}")

                        try:
                            # Try to get the numerical part (handling possible units)
                            actual_base_length_value = float(str(actual_base_length).split()[0]) if isinstance(actual_base_length, str) else float(actual_base_length)
                            actual_base_width_value = float(str(actual_base_width).split()[0]) if isinstance(actual_base_width, str) else float(actual_base_width)
                            actual_pyramid_height_value = float(str(actual_pyramid_height).split()[0]) if isinstance(actual_pyramid_height, str) else float(actual_pyramid_height)
                            
                            # Update validation logic to support pyramids created through wedge
                            # The base of the wedge is on the XZ plane, but dimensions may need adjusted interpretation
                            if actual_base_length_value > 0 and actual_base_width_value > 0 and actual_pyramid_height_value > 0:
                                # Relax validation standards, allow larger dimensional errors
                                base_length_error = abs((actual_base_length_value - expected_base_length) / expected_base_length) <= 0.1
                                
                                # For pyramids created as wedges, width and height might be swapped
                                # Check both possibilities: normal match or swapped dimensions
                                normal_width_match = abs((actual_base_width_value - expected_base_width) / expected_base_width) <= 0.1
                                height_as_width_match = abs((actual_base_width_value - expected_pyramid_height) / expected_pyramid_height) <= 0.1
                                normal_height_match = abs((actual_pyramid_height_value - expected_pyramid_height) / expected_pyramid_height) <= 0.1
                                width_as_height_match = abs((actual_pyramid_height_value - expected_base_width) / expected_base_width) <= 0.1
                                
                                # If either matching method is correct, consider it right
                                base_width_error = normal_width_match or height_as_width_match
                                pyramid_height_error = normal_height_match or width_as_height_match
                                
                                logger.info(f"Dimension validation: Length match={base_length_error}, normal width match={normal_width_match}, height as width match={height_as_width_match}")
                                logger.info(f"Dimension validation: Normal height match={normal_height_match}, width as height match={width_as_height_match}")
                                
                                # For pyramids created as wedges on XZ plane, assume mirror plane is XZ
                                mirror_plane_correct = True
                                has_mirror_correct = actual_has_mirror
                            else:
                                base_length_error = False
                                base_width_error = False
                                pyramid_height_error = False
                                mirror_plane_correct = False
                                has_mirror_correct = False
                        except Exception as e:
                            logger.error(f"Parameter comparison error: {str(e)}")
                            base_length_error = False
                            base_width_error = False
                            pyramid_height_error = False
                            mirror_plane_correct = False
                            has_mirror_correct = False
                        
                        pyramid_correct = base_length_error and base_width_error and pyramid_height_error
                        mirror_correct = mirror_plane_correct and has_mirror_correct
                        
                        if pyramid_correct and mirror_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Created pyramid with mirror feature and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created a pyramid with mirror feature meeting requirements and saved'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Pyramid correct: {pyramid_correct}, " +
                                          f"Mirror feature correct: {mirror_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