Task configuration:
{"task_id": "17", "task_name": "delete_color_sources", "description": "Batch delete three color sources", "application_info": {"name": "obs-studio", "executable_path": "apps/obs-studio/bin/64bit/obs64", "args": []}, "task_parameters": {"source_names": ["color1", "color2", "color3"]}, "instruction_template": "Delete multiple or single color sources named ${source_names} in obs-studio", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 120}, "total_steps": 1, "events": {"sources_deleted": {"description": "Successfully deleted all specified color sources", "is_key_step": true, "key_step_index": 1, "key_step_name": "Color sources deleted successfully"}}, "mcp_servers": [{"command": "node", "args": ["/home/agent/obs-mcp/build/index.js"]}]}

Hook script:
(function () {
    // Script settings
    const EVENT_ON_ENTER = "function called";
    const EVENT_ON_LEAVE = "function returned";
    
    const MESSAGE_source_deleted = "Source has been deleted";
    const MESSAGE_script_initialized = "Monitoring script has started";
    const MESSAGE_hook_installed = "Monitoring hook installed, waiting for operation...";

    // Send events to the evaluation system
    function sendEvent(eventType, data = {}) {
        console.log("[Event]", eventType, JSON.stringify(data, null, 2));
        const payload = {
            event: eventType,
            ...data,
            timestamp: new Date().getTime()
        };
        send(payload);
    }

    // Get function address
    function getFunctionAddress(functionName) {
        console.log("[Debug] Searching for function:", functionName);
        const funcAddr = DebugSymbol.getFunctionByName(functionName);
        if (!funcAddr) {
            console.log("[Error] Function not found:", functionName);
            sendEvent("error", {
                error_type: "function_not_found",
                message: `Unable to find function ${functionName}`
            });
            return null;
        }

        console.log("[Debug] Found function address:", functionName, funcAddr);
        sendEvent("function_found", {
            address: funcAddr.toString(),
            message: `Found actual address of function ${functionName}`
        });
        return funcAddr;
    }

    // Get source of scene item
    function obs_sceneitem_get_source(item) {
        const func = new NativeFunction(
            getFunctionAddress("obs_sceneitem_get_source"),
            'pointer',
            ['pointer']
        );
        return func(item);
    }

    // Monitor deletion of scene items
    function hookSceneItemRemove() {
        console.log("[Hook] Setting up hook for obs_sceneitem_remove");
        const funcAddr = getFunctionAddress("obs_sceneitem_remove");
        if (!funcAddr) return;

        Interceptor.attach(funcAddr, {
            onEnter(args) {
                this.item = args[0];
                if (this.item) {
                    try {
                        const source = new OBSSource(obs_sceneitem_get_source(this.item));
                        const source_name = source.getName();
                        const source_id = source.getId();

                        console.log("[obs_sceneitem_remove] Source name:", source_name);
                        console.log("[obs_sceneitem_remove] Source type:", source_id);

                        // Only focus on color sources
                        if (source_id === "color_source_v3") {
                            sendEvent("source_deleted", {
                                source_name: source_name,
                                source_id: source_id,
                                message: MESSAGE_source_deleted
                            });
                        }
                    } catch (error) {
                        console.log("[Error] Failed to get source information:", error);
                    }
                }
            }
        });
    }

    function hookSourceRemove() {
        console.log("[Hook] Setting up hook for obs_source_remove");
        const funcAddr = getFunctionAddress("obs_source_remove");
        if (!funcAddr) return;

        Interceptor.attach(funcAddr, {
            onEnter(args) {
                this.source = args[0];
                if (this.source) {
                    try {
                        const source = new OBSSource(this.source);
                        const source_name = source.getName();
                        const source_id = source.getId();

                        console.log("[obs_source_remove] Source name:", source_name);
                        console.log("[obs_source_remove] Source type:", source_id);

                        // Only focus on color sources
                        if (source_id === "color_source_v3") {
                            sendEvent("source_deleted", {
                                source_name: source_name,
                                source_id: source_id,
                                message: MESSAGE_source_deleted
                            });
                        }
                    } catch (error) {
                        console.log("[Error] Failed to get source information:", error);
                    }
                }
            }
        });
    }

    // OBSSource class for operating OBS sources
    class OBSSource {
        constructor(ptr) {
            console.log("[OBSSource] Creating new instance, pointer:", ptr);
            this.ptr = ptr;
        }

        getName() {
            console.log("[OBSSource] Getting source name");
            const func = new NativeFunction(
                getFunctionAddress("obs_source_get_name"),
                'pointer',
                ['pointer']
            );
            const namePtr = func(this.ptr);
            const name = namePtr.readCString(-1);
            console.log("[OBSSource] Retrieved source name:", name);
            return name;
        }

        getId() {
            console.log("[OBSSource] Getting source ID");
            const func = new NativeFunction(
                getFunctionAddress("obs_source_get_id"),
                'pointer',
                ['pointer']
            );
            const idPtr = func(this.ptr);
            const id = idPtr.readCString(-1);
            console.log("[OBSSource] Retrieved source ID:", id);
            return id;
        }
    }

    // Initialize hooks
    function initHook() {
        console.log("[Init] Starting hook initialization");
        sendEvent("script_initialized", {
            message: MESSAGE_script_initialized
        });

        // Initialize hooks
        hookSceneItemRemove();
        hookSourceRemove();

        console.log("[Init] Hook initialization completed");
        sendEvent("hook_installed", {
            message: MESSAGE_hook_installed
        });
    }

    // Start script
    console.log("[Start] Script execution started");
    initHook();
    console.log("[Start] Script execution completed, waiting for events...");
})();

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

from typing import Dict, Any, Optional, List

_DELETED_SOURCES = set()  # Used to track deleted sources


def message_handler(
    message: Dict[str, Any], logger, task_parameter: Dict[str, Any]
) -> Optional[List[Dict[str, Any]]]:
    payload = message["payload"]
    event_type = payload["event"]
    logger.debug(f"Received event: {event_type}")
    # Handle source deletion event
    if event_type == "source_deleted":
        source_name = payload.get("source_name")
        if source_name in task_parameter["source_names"]:
            _DELETED_SOURCES.add(source_name)
            logger.info(f"Source {source_name} has been deleted")

            # Check if all required sources have been deleted
            if _DELETED_SOURCES == set(task_parameter["source_names"]):
                return [
                    {"status": "key_step", "index": 1},
                    {
                        "status": "success",
                        "reason": "Successfully deleted color sources",
                    },
                ]

    return None