Task configuration:
{"task_id": "18", "task_name": "create_octagonal_prism_with_holes", "description": "Create an octagonal prism with a central circular hole in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task18.FCStd", "prism_radius": 50.0, "prism_height": 20.0, "hole_radius": 15.0, "sides_count": 8}, "instruction_template": "The application is already open. Create an octagonal prism in FreeCAD with a circumscribed circle radius of ${prism_radius}mm and height of ${prism_height}mm, then add a circular through-hole with radius ${hole_radius}mm in the center of the prism. The prism can be created using 'create a additive prism'(typeid), and the circular hole should be created using 'create a subtractive cylinder'(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": "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 octagonal prism with central circular hole 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 Octagonal Prism with Central Circular Hole Monitoring Hook Script
// Used to monitor FreeCAD operations for creating an octagonal prism with central circular hole and detect task completion
// After creating an octagonal prism with central circular hole and saving the file, the test program detects the save and checks if the document contains an octagonal prism with central circular hole that meets 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 event to 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 symbol
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);
      
      // If not found, report 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 the specified file
file_path = '${this.filename}'
doc = FreeCAD.open(file_path)

# Get active document
if doc is None:
    result = None
else:
    # Find octagonal prism and center hole
    prism = None
    hole = None
    has_hole = False
    hole_radius = 0.0
    sides_count = 0
    processed_objects = set()
    
    # Check all objects, looking for prism and hole
    for subobj in doc.Objects:
        if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "ShapeType"):
            # Look for polygonal prism
            if ('Prism' in subobj.TypeId or 'Extrusion' in subobj.TypeId 
                or 'Pad' in subobj.TypeId or 'AdditivePrism' 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 been processed, skip
                if obj_id in processed_objects:
                    continue
                processed_objects.add(obj_id)
                
                if subobj.Shape.ShapeType == "Solid":
                    # Try to get prism parameters
                    if hasattr(subobj, "Polygon"):
                        # If it's a polygon prism, get number of sides
                        sides_count = subobj.Polygon
                    elif hasattr(subobj, "Circumradius"):
                        # Get circumscribed radius
                        prism_radius = subobj.Circumradius.Value
                    elif hasattr(subobj, "Height"):
                        # Get height
                        prism_height = subobj.Height.Value
                    elif hasattr(subobj, "Length"):
                        # In some cases, length may represent height
                        prism_height = subobj.Length.Value
                    
                    # If parameters are incomplete, analyze shape
                    if sides_count == 0:
                        # Try to identify number of sides from shape
                        if hasattr(subobj.Shape, "Faces"):
                            # For a prism, there should be sides+2 faces (bottom, top, and side faces)
                            face_count = len(subobj.Shape.Faces)
                            if face_count >= 3:  # At least three faces
                                # Number of side faces may represent polygon sides
                                possible_sides = face_count - 2
                                if possible_sides >= 3:  # At least a triangle
                                    sides_count = possible_sides
            
            # Check if there is a center hole
            elif ('Pocket' in subobj.TypeId or 'SubtractiveCylinder' in subobj.TypeId 
                    or 'Cut' in subobj.TypeId or 'Hole' 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 been processed, skip
                if obj_id in processed_objects:
                    continue
                processed_objects.add(obj_id)
                
                # Mark that a hole exists
                has_hole = True
                
                # Try to get hole radius
                if hasattr(subobj, "Radius"):
                    hole_radius = subobj.Radius.Value
                elif hasattr(subobj, "Diameter"):
                    hole_radius = subobj.Diameter.Value / 2
    
    # If complete information cannot be obtained through attributes, try geometric analysis
    # Calculate the circumscribed circle radius and height of the prism
    if not prism:
        prism_radius = 0.0
        prism_height = 0.0
        
        # Traverse all objects in the document
        for obj in doc.Objects:
            if hasattr(obj, "Shape") and hasattr(obj.Shape, "BoundBox"):
                # Get bounding box
                bbox = obj.Shape.BoundBox
                
                # Calculate circumscribed circle radius in X-Y plane
                center_x = (bbox.XMin + bbox.XMax) / 2
                center_y = (bbox.YMin + bbox.YMax) / 2
                
                # Find the farthest vertex as the circumscribed circle radius
                max_radius = 0.0
                
                if hasattr(obj.Shape, "Vertexes"):
                    for vertex in obj.Shape.Vertexes:
                        dx = vertex.X - center_x
                        dy = vertex.Y - center_y
                        distance = math.sqrt(dx*dx + dy*dy)
                        max_radius = max(max_radius, distance)
                
                if max_radius > 0:
                    prism_radius = max_radius
                
                # Height is typically the dimension in the Z direction
                prism_height = bbox.ZMax - bbox.ZMin
                
                # Verify if it could be an octagonal prism
                if hasattr(obj.Shape, "Faces") and sides_count == 0:
                    # Count the number of faces
                    face_count = len(obj.Shape.Faces)
                    # A prism should have sides+2 faces
                    if face_count >= 5:  # At least a triangular prism (5 faces)
                        sides_count = face_count - 2
    
    # Verify center hole - check if hole is close to center
    if not has_hole:
        # Try to detect center hole geometrically
        for obj in doc.Objects:
            if hasattr(obj, "Shape") and hasattr(obj.Shape, "Faces"):
                # Look for cylindrical inner surface, which might be a hole
                for face in obj.Shape.Faces:
                    if hasattr(face, "Surface") and hasattr(face.Surface, "Radius"):
                        # Found a cylindrical surface
                        potential_hole_radius = face.Surface.Radius
                        
                        # Check if this face is near the center of the object
                        if hasattr(face, "CenterOfMass"):
                            center_of_mass = face.CenterOfMass
                            # Check if the center of the face is close to the object center (XY plane)
                            if abs(center_of_mass.x) < 1.0 and abs(center_of_mass.y) < 1.0:
                                has_hole = True
                                hole_radius = potential_hole_radius
                                sides_count -= 1
                                break
    
    # Return result - ensure pure numbers are returned rather than values with units
    # Handle values that may have units
    def extract_value(val):
        if val is None:
            return None
        try:
            # If it's a string form of a value with units (e.g. "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 to convert to float
            return float(val)
        except:
            # If conversion fails, return the original value
            return val
    
    result = {
        'prism_radius': extract_value(prism_radius),
        'prism_height': extract_value(prism_height),
        'hole_radius': extract_value(hole_radius),
        'sides_count': sides_count,
        'has_hole': has_hole
    }
                    `
                    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 completed, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }
  
  // Execute hook initialization immediately
  initHook();
})();


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

"""
FreeCAD Octagonal Prism with Central Circular Hole 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
PRISM_RADIUS = "prism_radius"
PRISM_HEIGHT = "prism_height"
HOLE_RADIUS = "hole_radius"
SIDES_COUNT = "sides_count"
HAS_HOLE = "has_hole"

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 object
        
    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("Octagonal prism with central circular hole object not found")
            return None
            
        # Validate result format
        required_keys = [
            PRISM_RADIUS, PRISM_HEIGHT, HOLE_RADIUS, SIDES_COUNT, HAS_HOLE
        ]
        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 object
        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"Event received: {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"Detected keywords, document path: {filename}, expected document path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if octagonal prism parameters meet expectations
                        expected_prism_radius = task_parameter.get(PRISM_RADIUS, 50.0)
                        expected_prism_height = task_parameter.get(PRISM_HEIGHT, 20.0)
                        expected_hole_radius = task_parameter.get(HOLE_RADIUS, 15.0)
                        expected_sides_count = task_parameter.get(SIDES_COUNT, 8)

                        actual_prism_radius = result[PRISM_RADIUS]
                        actual_prism_height = result[PRISM_HEIGHT]
                        actual_hole_radius = result[HOLE_RADIUS]
                        actual_sides_count = result[SIDES_COUNT]
                        actual_has_hole = result[HAS_HOLE]

                        # Log key parameters
                        logger.info(f"Task parameter check: Expected prism circumscribed radius={expected_prism_radius}, height={expected_prism_height}, sides={expected_sides_count}")
                        logger.info(f"Expected center hole: radius={expected_hole_radius}")
                        logger.info(f"Actual parameters: Actual prism circumscribed radius={actual_prism_radius}, height={actual_prism_height}, sides={actual_sides_count}")
                        logger.info(f"Actual center hole: radius={actual_hole_radius}, has hole: {actual_has_hole}")

                        try:
                            # Try to get numeric part (handling possible units)
                            actual_prism_radius_value = float(str(actual_prism_radius).split()[0]) if isinstance(actual_prism_radius, str) else float(actual_prism_radius)
                            actual_prism_height_value = float(str(actual_prism_height).split()[0]) if isinstance(actual_prism_height, str) else float(actual_prism_height)
                            actual_hole_radius_value = float(str(actual_hole_radius).split()[0]) if isinstance(actual_hole_radius, str) else float(actual_hole_radius)
                            actual_sides_count_value = int(actual_sides_count)
                            
                            # Allow a certain margin of error (0.01%)
                            prism_radius_error = abs((actual_prism_radius_value - expected_prism_radius) / expected_prism_radius) <= 0.0001
                            prism_height_error = abs((actual_prism_height_value - expected_prism_height) / expected_prism_height) <= 0.0001
                            hole_radius_error = abs((actual_hole_radius_value - expected_hole_radius) / expected_hole_radius) <= 0.0001
                            sides_count_error = actual_sides_count_value == expected_sides_count
                            
                            has_hole_correct = actual_has_hole
                        except Exception as e:
                            logger.error(f"Parameter comparison error: {str(e)}")
                            prism_radius_error = False
                            prism_height_error = False
                            hole_radius_error = False
                            sides_count_error = False
                            has_hole_correct = False
                        
                        prism_correct = prism_radius_error and prism_height_error and sides_count_error
                        hole_correct = hole_radius_error and has_hole_correct
                        
                        if prism_correct and hole_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Created octagonal prism with central circular hole and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created an octagonal prism with central circular hole according to requirements and saved it'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Octagonal prism correct: {prism_correct}, " +
                                          f"Central circular hole correct: {hole_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