Task configuration:
{"task_id": "20", "task_name": "create_prism_with_stepped_hole", "description": "Create a triangular prism with a stepped hole in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task20.FCStd", "prism_circumradius": 35.0, "prism_height": 40.0, "hole_inner_radius": 5.0, "hole_outer_radius": 10.0, "hole_depth": 15.0, "inner_hole_through": true}, "instruction_template": "The application is already open. In FreeCAD, create a triangular prism with an equilateral triangle as its base, with a circumradius of ${prism_circumradius}mm and a height of ${prism_height}mm. Then add a stepped hole in the center of its top face. The stepped hole consists of two parts: an inner small hole with a radius of ${hole_inner_radius}mm that goes through the entire prism, and an outer larger hole with a radius of ${hole_outer_radius}mm that extends from the top face to a depth of ${hole_depth}mm. The triangular prism can be created by 'create an additive prism' (typeid), and the stepped hole should be created using subtractive cylinders (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 triangular prism with stepped 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 Triangular Prism with Stepped Hole Monitoring Hook Script
// Used to monitor FreeCAD operations for creating a triangular prism with stepped hole and detect task completion
// After creating a triangular prism with stepped hole and saving the file, the test program detects the save operation and checks if the document contains a triangular prism with stepped hole 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;
      }

      // 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 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 triangular prism and stepped hole
    prism = None
    hole_inner_radius = 0.0
    hole_outer_radius = 0.0
    hole_depth = 0.0
    prism_circumradius = 0.0
    prism_height = 0.0
    inner_hole_through = False
    
    # Store detected cylinders for later analysis
    cylinders = []

    # First check all objects, looking for triangular prism and cylinders
    for obj in doc.Objects:
        # Check if it's a solid object
        if hasattr(obj, "Shape") and obj.Shape.ShapeType == "Solid":
            # Check if it's a triangular prism
            if hasattr(obj, "TypeId"):
                # Directly check if it's a prism object
                if "Prism" in obj.TypeId:
                    prism = obj
                    if hasattr(obj, "Circumradius"):
                        prism_circumradius = obj.Circumradius.Value
                    if hasattr(obj, "Height"):
                        prism_height = obj.Height.Value
                
                # Or check if it's a prism-like object formed by extruding a polygon
                elif "Extrude" in obj.TypeId or "Pad" in obj.TypeId:
                    # For extruded shapes, check the number of vertices and faces
                    if hasattr(obj.Shape, "Vertexes") and hasattr(obj.Shape, "Faces"):
                        # A triangular prism should have 6 vertices and 5 faces (3 quadrilateral sides + 2 triangular bases)
                        if len(obj.Shape.Vertexes) == 6 and (len(obj.Shape.Faces) == 5 or len(obj.Shape.Faces) == 8):
                            prism = obj
                            # Use bounding box to get approximate dimensions
                            if hasattr(obj.Shape, "BoundBox"):
                                bbox = obj.Shape.BoundBox
                                # Estimate circumradius as approximately the radius of the circle around the triangle, about half to two-thirds of the longest side
                                max_dim = max(bbox.XLength, bbox.ZLength)
                                prism_circumradius = max_dim / 2
                                # Height is typically in Y direction
                                prism_height = bbox.YLength
                
                # Check if it's a cylinder (might be a hole)
                elif "Cylinder" in obj.TypeId or "Hole" in obj.TypeId:
                    if hasattr(obj, "Radius") and hasattr(obj, "Height"):
                        radius = obj.Radius.Value
                        height = obj.Height.Value
                        # Store all cylinders for later processing
                        cylinders.append({
                            'radius': radius,
                            'height': height,
                            'obj': obj
                        })
            
            # If the object doesn't have a clear TypeId or is not a standard type, try to determine by shape characteristics
            elif hasattr(obj.Shape, "Vertexes") and hasattr(obj.Shape, "Faces"):
                # Check if it might be a triangular prism (6 vertices, 5 faces)
                if len(obj.Shape.Vertexes) == 6 and len(obj.Shape.Faces) == 5:
                    if prism is None: # If we haven't found a prism yet
                        prism = obj
                        # Use bounding box to get approximate dimensions
                        if hasattr(obj.Shape, "BoundBox"):
                            bbox = obj.Shape.BoundBox
                            # Estimate circumradius as approximately the radius of the circle around the triangle, about half to two-thirds of the longest side
                            max_dim = max(bbox.XLength, bbox.ZLength)
                            prism_circumradius = max_dim / 2
                            # Height is typically in Y direction
                            prism_height = bbox.YLength
    
    # Update logic for determining if the small hole goes through
    if len(cylinders) > 0:
        # Sort by radius, smaller ones might be inner holes, larger ones might be outer holes
        cylinders.sort(key=lambda x: x['radius'])

        if len(cylinders) >= 2:
            inner_cylinder = cylinders[0]
            outer_cylinder = cylinders[1]

            hole_inner_radius = inner_cylinder['radius']
            hole_outer_radius = outer_cylinder['radius']
            hole_depth = outer_cylinder['height']

            # Check if the small hole goes through the triangular prism
            if prism is not None and hasattr(prism.Shape, "BoundBox"):
                prism_bbox = prism.Shape.BoundBox
                inner_cylinder_bbox = inner_cylinder['obj'].Shape.BoundBox

                # Check if the bottom of the small hole touches the bottom of the prism
                bottom_contact = abs(inner_cylinder_bbox.ZMin - prism_bbox.ZMin) < 0.1

                # Check if the top of the small hole is higher than or touches the bottom of the large hole
                top_contact = inner_cylinder_bbox.ZMax >= outer_cylinder['obj'].Shape.BoundBox.ZMin

                inner_hole_through = bottom_contact and top_contact

        elif len(cylinders) == 1:
            cylinder = cylinders[0]
            hole_inner_radius = cylinder['radius']

            if prism is not None and hasattr(prism.Shape, "BoundBox"):
                prism_bbox = prism.Shape.BoundBox
                cylinder_bbox = cylinder['obj'].Shape.BoundBox

                # Check if the bottom of the hole touches the bottom of the prism
                bottom_contact = abs(cylinder_bbox.ZMin - prism_bbox.ZMin) < 0.1

                # Check if the top of the hole is higher than or touches the top of the prism (assuming there's only one hole, it touches the top of the prism)
                top_contact = cylinder_bbox.ZMax >= prism_bbox.ZMax

                inner_hole_through = bottom_contact and top_contact

    result = {
        'prism_circumradius': prism_circumradius,
        'prism_height': prism_height,
        'hole_inner_radius': hole_inner_radius,
        'hole_outer_radius': hole_outer_radius,
        'hole_depth': hole_depth,
        'inner_hole_through': inner_hole_through
    }`;
                    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 installation complete, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }

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


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

"""
FreeCAD Triangular Prism with Stepped 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_CIRCUMRADIUS = "prism_circumradius"
PRISM_HEIGHT = "prism_height"
HOLE_INNER_RADIUS = "hole_inner_radius"
HOLE_OUTER_RADIUS = "hole_outer_radius"
HOLE_DEPTH = "hole_depth"
INNER_HOLE_THROUGH = "inner_hole_through"


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("Triangular prism with stepped hole object not found")
            return None
            
        # Validate result format
        required_keys = [
            PRISM_CIRCUMRADIUS, PRISM_HEIGHT, HOLE_INNER_RADIUS, HOLE_OUTER_RADIUS, HOLE_DEPTH, INNER_HOLE_THROUGH
        ]
        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 prism parameters match expectations
                        expected_prism_circumradius = task_parameter.get(PRISM_CIRCUMRADIUS, 35.0)
                        expected_prism_height = task_parameter.get(PRISM_HEIGHT, 40.0)
                        expected_hole_inner_radius = task_parameter.get(HOLE_INNER_RADIUS, 5.0)
                        expected_hole_outer_radius = task_parameter.get(HOLE_OUTER_RADIUS, 10.0)
                        expected_hole_depth = task_parameter.get(HOLE_DEPTH, 15.0)
                        expected_inner_hole_through = task_parameter.get(INNER_HOLE_THROUGH, True)

                        actual_prism_circumradius = result[PRISM_CIRCUMRADIUS]
                        actual_prism_height = result[PRISM_HEIGHT]
                        actual_hole_inner_radius = result[HOLE_INNER_RADIUS]
                        actual_hole_outer_radius = result[HOLE_OUTER_RADIUS]
                        actual_hole_depth = result[HOLE_DEPTH]
                        actual_inner_hole_through = result[INNER_HOLE_THROUGH]

                        # Log key parameters
                        logger.info(f"Task parameter check: Expected prism circumradius={expected_prism_circumradius}, height={expected_prism_height}")
                        logger.info(f"Expected stepped hole inner radius={expected_hole_inner_radius}, outer radius={expected_hole_outer_radius}, depth={expected_hole_depth}")
                        logger.info(f"Expected inner hole through: {expected_inner_hole_through}")
                        logger.info(f"Actual parameters: Actual prism circumradius={actual_prism_circumradius}, height={actual_prism_height}")
                        logger.info(f"Actual stepped hole inner radius={actual_hole_inner_radius}, outer radius={actual_hole_outer_radius}, depth={actual_hole_depth}")
                        logger.info(f"Actual inner hole through: {actual_inner_hole_through}")

                        # Validate parameters
                        # Directly compare circumradius and height
                        prism_correct = (abs(actual_prism_circumradius - expected_prism_circumradius) <= 1.0 and
                                        abs(actual_prism_height - expected_prism_height) <= 1.0)
                        hole_correct = (abs(actual_hole_inner_radius - expected_hole_inner_radius) <= 0.5 and
                                        abs(actual_hole_outer_radius - expected_hole_outer_radius) <= 0.5 and
                                        abs(actual_hole_depth - expected_hole_depth) <= 1.0 and
                                        actual_inner_hole_through == expected_inner_hole_through)

                        if prism_correct and hole_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Created triangular prism with stepped hole and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created a triangular prism with a stepped hole meeting requirements and saved'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Prism correct: {prism_correct}, " +
                                          f"Stepped 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