/**
 * Tool Execution Orchestrator Module
 *
 * Manages the agentic tool execution loop, handling:
 * - Tool call batching and execution ordering
 * - Parallel vs sequential execution decisions
 * - Tool result collection and formatting
 * - Execution metrics and cost tracking
 *
 * @module agent/execution
 */
import { EventEmitter } from "events";
import { getErrorMessage } from "../../errors/index.js";
import { logger } from "../../utils/logger.js";
/**
 * Default orchestrator configuration
 */
export const DEFAULT_ORCHESTRATOR_CONFIG = {
    maxToolRounds: 50,
    parallelExecution: true,
    toolTimeout: 120000, // 2 minutes
    maxConcurrent: 5,
};
/**
 * Tools that are safe to run in parallel (read-only operations)
 */
const SAFE_PARALLEL_TOOLS = new Set([
    "view_file",
    "search",
    "web_search",
    "web_fetch",
    "codebase_map",
    "pdf",
    "audio",
    "video",
    "document",
    "ocr",
    "qr",
    "archive",
    "clipboard",
]);
/**
 * Tools that modify state (unsafe for parallel execution)
 */
const WRITE_TOOLS = new Set([
    "create_file",
    "str_replace_editor",
    "edit_file",
    "multi_edit",
    "bash",
    "git",
    "create_todo_list",
    "update_todo_list",
    "screenshot",
    "export",
    "diagram",
]);
/**
 * Fast tools that typically complete in <100ms
 * Used for optimization hints
 */
const FAST_TOOLS = new Set([
    "view_file",
    "codebase_map",
    "clipboard",
]);
/**
 * Slow tools that may benefit from higher timeout
 * Used for optimization hints
 */
const SLOW_TOOLS = new Set([
    "web_search",
    "web_fetch",
    "bash",
    "search",
    "pdf",
    "audio",
    "video",
]);
/**
 * ToolExecutionOrchestrator - Manages the agentic tool execution loop
 *
 * This class handles the orchestration of tool execution during an
 * agent conversation, including:
 * - Deciding when to execute tools in parallel vs sequentially
 * - Managing execution rounds and preventing infinite loops
 * - Collecting and aggregating tool results
 * - Tracking execution metrics
 *
 * @example
 * ```typescript
 * const orchestrator = new ToolExecutionOrchestrator({
 *   maxToolRounds: 50,
 *   parallelExecution: true
 * });
 *
 * orchestrator.setExecutor(async (toolCall) => {
 *   // Execute the tool and return result
 *   return { success: true, output: "done" };
 * });
 *
 * const results = await orchestrator.executeBatch(toolCalls);
 * ```
 */
export class ToolExecutionOrchestrator extends EventEmitter {
    config;
    executor = null;
    currentRound = 0;
    metrics;
    aborted = false;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_ORCHESTRATOR_CONFIG, ...config };
        this.metrics = this.createEmptyMetrics();
    }
    /**
     * Set the tool executor function
     */
    setExecutor(executor) {
        this.executor = executor;
    }
    /**
     * Check if tool calls can be safely executed in parallel
     *
     * Tools that modify the same files or have side effects should not be parallelized.
     * Read-only operations (view_file, search, web_search) are safe to parallelize.
     */
    canParallelizeToolCalls(toolCalls) {
        if (!this.config.parallelExecution || toolCalls.length <= 1) {
            return false;
        }
        // Check if all tools are safe for parallel execution
        const allSafe = toolCalls.every(tc => SAFE_PARALLEL_TOOLS.has(tc.function.name));
        if (allSafe)
            return true;
        // Check if any write tools target the same file
        const writeToolCalls = toolCalls.filter(tc => WRITE_TOOLS.has(tc.function.name));
        if (writeToolCalls.length > 1) {
            // Extract file paths from arguments
            const filePaths = new Set();
            for (const tc of writeToolCalls) {
                try {
                    const args = JSON.parse(tc.function.arguments);
                    const path = args.path || args.target_file || args.file_path;
                    if (path) {
                        if (filePaths.has(path)) {
                            return false; // Same file targeted by multiple write tools
                        }
                        filePaths.add(path);
                    }
                }
                catch {
                    return false; // Can't parse args, be safe
                }
            }
            // Multiple write tools to different files - can parallelize
            return true;
        }
        // If there's only one write tool, safe to parallelize with read tools
        return writeToolCalls.length <= 1;
    }
    /**
     * Execute a batch of tool calls
     *
     * @param toolCalls - Array of tool calls to execute
     * @returns BatchExecutionResult with all results
     */
    async executeBatch(toolCalls) {
        if (!this.executor) {
            throw new Error("Tool executor not set. Call setExecutor() first.");
        }
        const startTime = Date.now();
        const results = new Map();
        this.currentRound++;
        this.metrics.toolRounds++;
        this.emit("round:start", {
            round: this.currentRound,
            toolCount: toolCalls.length,
        });
        const canParallelize = this.canParallelizeToolCalls(toolCalls);
        if (canParallelize) {
            // Execute in parallel with concurrency limit
            const concurrencyLimit = Math.min(this.config.maxConcurrent, toolCalls.length);
            if (toolCalls.length <= concurrencyLimit) {
                // Simple parallel execution for small batches
                const promises = toolCalls.map(async (toolCall) => {
                    return this.executeSingleTool(toolCall);
                });
                const settled = await Promise.all(promises);
                for (const { id, result } of settled) {
                    results.set(id, result);
                }
            }
            else {
                // Use controlled concurrency for larger batches
                const executing = [];
                const queue = [...toolCalls];
                while (queue.length > 0 || executing.length > 0) {
                    // Fill up to concurrency limit
                    while (executing.length < concurrencyLimit && queue.length > 0) {
                        const toolCall = queue.shift();
                        const promise = this.executeSingleTool(toolCall)
                            .then(({ id, result }) => {
                            results.set(id, result);
                        })
                            .finally(() => {
                            const index = executing.indexOf(promise);
                            if (index !== -1)
                                executing.splice(index, 1);
                        });
                        executing.push(promise);
                    }
                    // Wait for at least one to complete
                    if (executing.length > 0) {
                        await Promise.race(executing);
                    }
                }
            }
        }
        else {
            // Execute sequentially
            for (const toolCall of toolCalls) {
                if (this.aborted) {
                    results.set(toolCall.id, {
                        success: false,
                        error: "Execution aborted",
                    });
                    continue;
                }
                const toolStartTime = Date.now();
                this.emit("tool:start", { toolCall });
                try {
                    const result = await this.executeWithTimeout(toolCall);
                    const duration = Date.now() - toolStartTime;
                    results.set(toolCall.id, result);
                    this.emit("tool:complete", { toolCall, result, duration });
                    this.updateToolStats(toolCall.function.name, result.success, duration);
                }
                catch (error) {
                    const duration = Date.now() - toolStartTime;
                    const errorResult = {
                        success: false,
                        error: `Tool execution failed: ${getErrorMessage(error)}`,
                    };
                    results.set(toolCall.id, errorResult);
                    this.emit("tool:error", {
                        toolCall,
                        error: getErrorMessage(error),
                    });
                    this.updateToolStats(toolCall.function.name, false, duration);
                }
            }
        }
        const duration = Date.now() - startTime;
        let successCount = 0;
        let failureCount = 0;
        for (const result of results.values()) {
            if (result.success) {
                successCount++;
                this.metrics.successfulCalls++;
            }
            else {
                failureCount++;
                this.metrics.failedCalls++;
            }
        }
        this.metrics.totalCalls += toolCalls.length;
        this.metrics.totalExecutionTime += duration;
        const batchResult = {
            results,
            duration,
            successCount,
            failureCount,
            parallel: canParallelize,
        };
        this.emit("round:complete", {
            round: this.currentRound,
            results: batchResult,
        });
        return batchResult;
    }
    /**
     * Execute a single tool call with full tracking
     */
    async executeSingleTool(toolCall) {
        const toolStartTime = Date.now();
        this.emit("tool:start", { toolCall });
        try {
            const result = await this.executeWithTimeout(toolCall);
            const duration = Date.now() - toolStartTime;
            this.emit("tool:complete", { toolCall, result, duration });
            this.updateToolStats(toolCall.function.name, result.success, duration);
            return { id: toolCall.id, result };
        }
        catch (error) {
            const duration = Date.now() - toolStartTime;
            const errorResult = {
                success: false,
                error: `Tool execution failed: ${getErrorMessage(error)}`,
            };
            this.emit("tool:error", {
                toolCall,
                error: getErrorMessage(error),
            });
            this.updateToolStats(toolCall.function.name, false, duration);
            return { id: toolCall.id, result: errorResult };
        }
    }
    /**
     * Execute a single tool call with timeout
     */
    async executeWithTimeout(toolCall) {
        if (!this.executor) {
            throw new Error("Tool executor not set");
        }
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => reject(new Error(`Tool execution timeout after ${this.config.toolTimeout}ms`)), this.config.toolTimeout);
        });
        return Promise.race([
            this.executor(toolCall),
            timeoutPromise,
        ]);
    }
    /**
     * Check if maximum tool rounds has been reached
     */
    hasReachedMaxRounds() {
        if (this.currentRound >= this.config.maxToolRounds) {
            this.emit("max_rounds_reached", { rounds: this.currentRound });
            return true;
        }
        return false;
    }
    /**
     * Get current round number
     */
    getCurrentRound() {
        return this.currentRound;
    }
    /**
     * Get remaining rounds
     */
    getRemainingRounds() {
        return Math.max(0, this.config.maxToolRounds - this.currentRound);
    }
    /**
     * Reset round counter for a new conversation
     */
    resetRounds() {
        this.currentRound = 0;
        this.aborted = false;
    }
    /**
     * Abort current execution
     */
    abort() {
        this.aborted = true;
        logger.info("Tool execution aborted");
    }
    /**
     * Check if execution was aborted
     */
    isAborted() {
        return this.aborted;
    }
    /**
     * Get execution metrics
     */
    getMetrics() {
        return {
            ...this.metrics,
            toolStats: new Map(this.metrics.toolStats),
        };
    }
    /**
     * Reset metrics
     */
    resetMetrics() {
        this.metrics = this.createEmptyMetrics();
    }
    /**
     * Get configuration
     */
    getConfig() {
        return { ...this.config };
    }
    /**
     * Update configuration
     */
    updateConfig(config) {
        this.config = { ...this.config, ...config };
    }
    /**
     * Get optimization hints based on tool execution patterns
     */
    getOptimizationHints() {
        const hints = [];
        const stats = this.metrics.toolStats;
        // Check for slow tools
        for (const [tool, stat] of stats) {
            if (stat.avgTime > 1000) {
                hints.push(`Tool "${tool}" is slow (avg ${stat.avgTime.toFixed(0)}ms). Consider caching.`);
            }
        }
        // Check for high failure rates
        for (const [tool, stat] of stats) {
            const failRate = stat.failures / stat.calls;
            if (stat.calls >= 3 && failRate > 0.3) {
                hints.push(`Tool "${tool}" has high failure rate (${(failRate * 100).toFixed(0)}%).`);
            }
        }
        // Check for tools that could be parallelized
        const sequentialWriteTools = [...stats.entries()]
            .filter(([tool]) => WRITE_TOOLS.has(tool))
            .filter(([_, stat]) => stat.calls > 3);
        if (sequentialWriteTools.length > 1) {
            hints.push('Multiple write tools detected. Consider batching write operations.');
        }
        // Check parallelization efficiency
        const parallelTools = [...stats.entries()]
            .filter(([tool]) => SAFE_PARALLEL_TOOLS.has(tool));
        if (parallelTools.length > 3) {
            hints.push(`${parallelTools.length} parallelizable tools used. Ensure parallel execution is enabled.`);
        }
        return hints;
    }
    /**
     * Check if a tool is typically fast
     */
    isToolFast(toolName) {
        return FAST_TOOLS.has(toolName);
    }
    /**
     * Check if a tool is typically slow
     */
    isToolSlow(toolName) {
        return SLOW_TOOLS.has(toolName);
    }
    /**
     * Get recommended timeout for a tool
     */
    getRecommendedTimeout(toolName) {
        if (FAST_TOOLS.has(toolName)) {
            return Math.min(30000, this.config.toolTimeout);
        }
        if (SLOW_TOOLS.has(toolName)) {
            return Math.max(180000, this.config.toolTimeout);
        }
        return this.config.toolTimeout;
    }
    /**
     * Format metrics for display
     */
    formatMetrics() {
        const lines = [
            "Tool Execution Metrics",
            "======================",
            `Total Rounds: ${this.metrics.toolRounds}`,
            `Total Calls: ${this.metrics.totalCalls}`,
            `Successful: ${this.metrics.successfulCalls}`,
            `Failed: ${this.metrics.failedCalls}`,
            `Total Time: ${this.metrics.totalExecutionTime}ms`,
            "",
            "Per-Tool Statistics:",
        ];
        for (const [tool, stats] of this.metrics.toolStats) {
            lines.push(`  ${tool}: ${stats.calls} calls, ${stats.successes} success, ` +
                `${stats.failures} failed, avg ${stats.avgTime.toFixed(0)}ms`);
        }
        return lines.join("\n");
    }
    /**
     * Create empty metrics object
     */
    createEmptyMetrics() {
        return {
            toolRounds: 0,
            totalCalls: 0,
            successfulCalls: 0,
            failedCalls: 0,
            totalExecutionTime: 0,
            toolStats: new Map(),
        };
    }
    /**
     * Update per-tool statistics
     */
    updateToolStats(toolName, success, duration) {
        const existing = this.metrics.toolStats.get(toolName) || {
            calls: 0,
            successes: 0,
            failures: 0,
            totalTime: 0,
            avgTime: 0,
        };
        existing.calls++;
        if (success) {
            existing.successes++;
        }
        else {
            existing.failures++;
        }
        existing.totalTime += duration;
        existing.avgTime = existing.totalTime / existing.calls;
        this.metrics.toolStats.set(toolName, existing);
    }
    /**
     * Dispose and cleanup
     */
    dispose() {
        this.removeAllListeners();
        this.executor = null;
        this.aborted = true;
    }
}
/**
 * Create a ToolExecutionOrchestrator instance
 */
export function createToolOrchestrator(config) {
    return new ToolExecutionOrchestrator(config);
}
// Singleton instance
let orchestratorInstance = null;
/**
 * Get global orchestrator instance
 */
export function getToolOrchestrator(config) {
    if (!orchestratorInstance) {
        orchestratorInstance = createToolOrchestrator(config);
    }
    return orchestratorInstance;
}
/**
 * Reset global orchestrator
 */
export function resetToolOrchestrator() {
    if (orchestratorInstance) {
        orchestratorInstance.dispose();
    }
    orchestratorInstance = null;
}
//# sourceMappingURL=tool-orchestrator.js.map