/**
 * Agent Executor Module
 *
 * Implements the core agentic loop for processing user messages,
 * both sequential and streaming. Handles tool execution rounds,
 * token counting, cost tracking, and context management.
 *
 * @module agent/execution
 */
import { logger } from "../../utils/logger.js";
import { getErrorMessage } from "../../errors/index.js";
import { sanitizeToolResult } from "../../utils/sanitize.js";
/**
 * AgentExecutor implements the core agentic loop
 *
 * The agentic loop follows this pattern:
 * 1. Select relevant tools for the query (RAG-based)
 * 2. Send message to LLM with selected tools
 * 3. If LLM requests tool calls, execute them
 * 4. Send tool results back to LLM
 * 5. Repeat until LLM responds without tool calls or max rounds reached
 *
 * Supports both sequential (processUserMessage) and streaming
 * (processUserMessageStream) execution modes.
 */
export class AgentExecutor {
    deps;
    config;
    constructor(deps, config) {
        this.deps = deps;
        this.config = config;
    }
    /**
     * Get or set the middleware pipeline.
     * Used by CodeBuddyAgent.enableAutoObservation() to inject middleware.
     */
    getMiddlewarePipeline() {
        return this.deps.middlewarePipeline;
    }
    setMiddlewarePipeline(pipeline) {
        this.deps.middlewarePipeline = pipeline;
    }
    /**
     * Build a MiddlewareContext from current loop state.
     */
    buildMiddlewareContext(toolRound, inputTokens, outputTokens, history, messages, isStreaming, abortController) {
        return {
            toolRound,
            maxToolRounds: this.config.maxToolRounds,
            sessionCost: this.config.getSessionCost(),
            sessionCostLimit: this.config.getSessionCostLimit(),
            inputTokens,
            outputTokens,
            history,
            messages,
            isStreaming,
            abortController,
        };
    }
    /**
     * Execute a tool call, optionally through the LaneQueue for serialization.
     * Read-only tools (grep, glob, read_file) run in parallel; mutating tools run serially.
     */
    executeToolViaLane(toolCall) {
        const laneQueue = this.deps.laneQueue;
        if (!laneQueue) {
            return this.deps.toolHandler.executeTool(toolCall);
        }
        const laneId = this.deps.laneId ?? 'default';
        const readOnlyTools = new Set([
            'grep', 'glob', 'read_file', 'list_files', 'search_files',
            'get_file_info', 'tree', 'find_references',
        ]);
        const isParallel = readOnlyTools.has(toolCall.function.name);
        return laneQueue.enqueue(laneId, () => this.deps.toolHandler.executeTool(toolCall), {
            parallel: isParallel,
            category: toolCall.function.name,
            timeout: 120000,
        });
    }
    /**
     * Process a user message sequentially (non-streaming)
     *
     * @param message - The user's input message
     * @param history - Chat history array (modified in place)
     * @param messages - LLM message array (modified in place)
     * @returns Array of new chat entries created during this turn
     */
    async processUserMessage(message, history, messages) {
        const newEntries = [];
        const maxToolRounds = this.config.maxToolRounds;
        let toolRounds = 0;
        // Track token usage for cost calculation
        const inputTokens = this.deps.tokenCounter.countMessageTokens(messages);
        let totalOutputTokens = 0;
        try {
            // Use RAG-based tool selection for initial query
            const selectionResult = await this.deps.toolSelectionStrategy.selectToolsForQuery(message);
            const tools = selectionResult.tools;
            this.deps.toolSelectionStrategy.cacheTools(tools);
            // Apply context management
            const preparedMessages = this.deps.contextManager.prepareMessages(messages);
            // Check for context warnings
            const contextWarning = this.deps.contextManager.shouldWarn(preparedMessages);
            if (contextWarning.warn) {
                logger.warn(contextWarning.message);
            }
            let currentResponse = await this.deps.client.chat(preparedMessages, tools, undefined, this.config.isGrokModel() && this.deps.toolSelectionStrategy.shouldUseSearchFor(message)
                ? { search_parameters: { mode: "auto" } }
                : { search_parameters: { mode: "off" } });
            // Agent loop
            while (toolRounds < maxToolRounds) {
                const assistantMessage = currentResponse.choices[0]?.message;
                if (!assistantMessage) {
                    throw new Error("No response received from AI. The API may be unavailable or the request was incomplete. Please try again.");
                }
                // Track output tokens
                if (currentResponse.usage) {
                    totalOutputTokens += currentResponse.usage.completion_tokens || 0;
                }
                else if (assistantMessage.content) {
                    totalOutputTokens += this.deps.tokenCounter.countTokens(assistantMessage.content);
                }
                // Handle tool calls
                if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
                    toolRounds++;
                    // Add assistant message with tool calls
                    const assistantEntry = {
                        type: "assistant",
                        content: assistantMessage.content || "Using tools to help you...",
                        timestamp: new Date(),
                        toolCalls: assistantMessage.tool_calls,
                    };
                    history.push(assistantEntry);
                    newEntries.push(assistantEntry);
                    // Add assistant message to conversation
                    messages.push({
                        role: "assistant",
                        content: assistantMessage.content || "",
                        tool_calls: assistantMessage.tool_calls,
                    });
                    // Pre-check cost limit before executing tools
                    this.config.recordSessionCost(inputTokens, totalOutputTokens);
                    if (this.config.isSessionCostLimitReached()) {
                        const sessionCost = this.config.getSessionCost();
                        const sessionCostLimit = this.config.getSessionCostLimit();
                        const costEntry = {
                            type: "assistant",
                            content: `Session cost limit reached ($${sessionCost.toFixed(2)} / $${sessionCostLimit.toFixed(2)}). Stopping before tool execution.`,
                            timestamp: new Date(),
                        };
                        history.push(costEntry);
                        messages.push({ role: "assistant", content: costEntry.content });
                        newEntries.push(costEntry);
                        break;
                    }
                    // Execute tool calls
                    for (const toolCall of assistantMessage.tool_calls) {
                        const toolCallEntry = {
                            type: "tool_call",
                            content: "Executing...",
                            timestamp: new Date(),
                            toolCall: toolCall,
                        };
                        const histIdx = history.length;
                        history.push(toolCallEntry);
                        const newIdx = newEntries.length;
                        newEntries.push(toolCallEntry);
                        const result = await this.executeToolViaLane(toolCall);
                        // Update entry with result
                        const updatedEntry = {
                            ...toolCallEntry,
                            type: "tool_result",
                            content: result.success ? result.output || "Success" : result.error || "Error occurred",
                            toolResult: result,
                        };
                        // Replace in history and newEntries using tracked indices (O(1))
                        history[histIdx] = updatedEntry;
                        newEntries[newIdx] = updatedEntry;
                        // Add tool result to messages
                        // Note: 'name' is required for Gemini API to match functionResponse with functionCall
                        messages.push({
                            role: "tool",
                            content: sanitizeToolResult(result.success ? result.output || "Success" : result.error || "Error"),
                            tool_call_id: toolCall.id,
                            name: toolCall.function.name,
                        });
                    }
                    // Get next response
                    const nextPreparedMessages = this.deps.contextManager.prepareMessages(messages);
                    currentResponse = await this.deps.client.chat(nextPreparedMessages, tools, undefined, this.config.isGrokModel() && this.deps.toolSelectionStrategy.shouldUseSearchFor(message)
                        ? { search_parameters: { mode: "auto" } }
                        : { search_parameters: { mode: "off" } });
                }
                else {
                    // No more tool calls
                    const finalEntry = {
                        type: "assistant",
                        content: assistantMessage.content || "I understand, but I don't have a specific response.",
                        timestamp: new Date(),
                    };
                    history.push(finalEntry);
                    messages.push({
                        role: "assistant",
                        content: assistantMessage.content || "",
                    });
                    newEntries.push(finalEntry);
                    break;
                }
            }
            if (toolRounds >= maxToolRounds) {
                const warningEntry = {
                    type: "assistant",
                    content: "Maximum tool execution rounds reached. Stopping to prevent infinite loops.",
                    timestamp: new Date(),
                };
                history.push(warningEntry);
                messages.push({ role: "assistant", content: warningEntry.content });
                newEntries.push(warningEntry);
            }
            // Record session cost
            this.config.recordSessionCost(inputTokens, totalOutputTokens);
            if (this.config.isSessionCostLimitReached()) {
                const sessionCost = this.config.getSessionCost();
                const sessionCostLimit = this.config.getSessionCostLimit();
                const costEntry = {
                    type: "assistant",
                    content: `💸 Session cost limit reached ($${sessionCost.toFixed(2)} / $${sessionCostLimit.toFixed(2)}). Please start a new session.`,
                    timestamp: new Date(),
                };
                history.push(costEntry);
                messages.push({ role: "assistant", content: costEntry.content });
                newEntries.push(costEntry);
            }
            return newEntries;
        }
        catch (error) {
            const errorMessage = getErrorMessage(error);
            const errorEntry = {
                type: "assistant",
                content: `Sorry, I encountered an error: ${errorMessage}`,
                timestamp: new Date(),
            };
            history.push(errorEntry);
            messages.push({ role: "assistant", content: errorEntry.content });
            return [errorEntry];
        }
    }
    /**
     * Process a user message with streaming response
     *
     * Yields chunks as they arrive from the LLM, enabling real-time UI updates.
     * Chunk types: 'content', 'tool_calls', 'tool_result', 'token_count', 'done'
     *
     * @param message - The user's input message
     * @param history - Chat history array (modified in place)
     * @param messages - LLM message array (modified in place)
     * @param abortController - Controller to cancel the operation
     * @yields Streaming chunks for UI consumption
     */
    async *processUserMessageStream(message, history, messages, abortController) {
        // Calculate input tokens
        let inputTokens = this.deps.tokenCounter.countMessageTokens(messages);
        yield {
            type: "token_count",
            tokenCount: inputTokens,
        };
        const maxToolRounds = this.config.maxToolRounds;
        let toolRounds = 0;
        let totalOutputTokens = 0;
        try {
            const pipeline = this.deps.middlewarePipeline;
            while (toolRounds < maxToolRounds) {
                if (abortController?.signal.aborted) {
                    yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                    yield { type: "done" };
                    return;
                }
                // Run before_turn middleware
                if (pipeline) {
                    const ctx = this.buildMiddlewareContext(toolRounds, inputTokens, totalOutputTokens, history, messages, true, abortController);
                    const mwResult = await pipeline.runBeforeTurn(ctx);
                    if (mwResult.action === 'stop') {
                        if (mwResult.message)
                            yield { type: "content", content: `\n\n${mwResult.message}` };
                        yield { type: "done" };
                        return;
                    }
                    if (mwResult.action === 'compact') {
                        // Trigger context compaction
                        this.deps.contextManager.prepareMessages(messages);
                    }
                    if (mwResult.action === 'warn' && mwResult.message) {
                        yield { type: "content", content: `\n${mwResult.message}\n` };
                    }
                }
                const selectionResult = await this.deps.toolSelectionStrategy.selectToolsForQuery(message);
                const tools = selectionResult.tools;
                if (toolRounds === 0)
                    this.deps.toolSelectionStrategy.cacheTools(tools);
                const preparedMessages = this.deps.contextManager.prepareMessages(messages);
                // Context warning is now handled by middleware, but keep fallback for non-pipeline mode
                if (!pipeline) {
                    const contextWarning = this.deps.contextManager.shouldWarn(preparedMessages);
                    if (contextWarning.warn) {
                        yield { type: "content", content: `\n${contextWarning.message}\n` };
                    }
                }
                const stream = this.deps.client.chatStream(preparedMessages, tools, undefined, this.config.isGrokModel() && this.deps.toolSelectionStrategy.shouldUseSearchFor(message)
                    ? { search_parameters: { mode: "auto" } }
                    : { search_parameters: { mode: "off" } });
                this.deps.streamingHandler.reset();
                for await (const chunk of stream) {
                    if (abortController?.signal.aborted) {
                        yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                        yield { type: "done" };
                        return;
                    }
                    const result = this.deps.streamingHandler.accumulateChunk(chunk);
                    if (result.reasoningContent) {
                        yield { type: "reasoning", reasoning: result.reasoningContent };
                    }
                    if (result.hasNewToolCalls && result.toolCalls) {
                        yield { type: "tool_calls", toolCalls: result.toolCalls };
                    }
                    if (result.displayContent) {
                        yield { type: "content", content: result.displayContent };
                    }
                    if (result.shouldEmitTokenCount && result.tokenCount !== undefined) {
                        yield { type: "token_count", tokenCount: inputTokens + result.tokenCount };
                    }
                }
                const extracted = this.deps.streamingHandler.extractToolCalls();
                if (extracted.toolCalls.length > 0) {
                    yield { type: "tool_calls", toolCalls: extracted.toolCalls };
                }
                const accumulatedMessage = this.deps.streamingHandler.getAccumulatedMessage();
                const content = accumulatedMessage.content || "Using tools to help you...";
                const toolCalls = accumulatedMessage.tool_calls;
                const assistantEntry = {
                    type: "assistant",
                    content: content,
                    timestamp: new Date(),
                    toolCalls: toolCalls,
                };
                history.push(assistantEntry);
                messages.push({ role: "assistant", content: content, tool_calls: toolCalls });
                if (toolCalls && toolCalls.length > 0) {
                    toolRounds++;
                    // Pre-check cost limit before executing tools
                    this.config.recordSessionCost(inputTokens, totalOutputTokens);
                    if (this.config.isSessionCostLimitReached()) {
                        const sessionCost = this.config.getSessionCost();
                        const sessionCostLimit = this.config.getSessionCostLimit();
                        yield { type: "content", content: `\n\nSession cost limit reached ($${sessionCost.toFixed(2)} / $${sessionCostLimit.toFixed(2)}). Stopping before tool execution.` };
                        yield { type: "done" };
                        return;
                    }
                    // Check for steering messages (steer mode: interrupt execution)
                    const mq = this.deps.messageQueue;
                    if (mq?.hasSteeringMessage()) {
                        const steering = mq.consumeSteeringMessage();
                        if (steering) {
                            yield { type: "steer", steer: { content: steering.content, source: steering.source } };
                            // Inject as user message and skip remaining tool calls
                            messages.push({ role: "user", content: steering.content });
                            history.push({
                                type: "user",
                                content: steering.content,
                                timestamp: new Date(),
                            });
                            continue; // Re-enter loop to get new LLM response
                        }
                    }
                    if (!this.deps.streamingHandler.hasYieldedToolCalls()) {
                        yield { type: "tool_calls", toolCalls: toolCalls };
                    }
                    for (const toolCall of toolCalls) {
                        if (abortController?.signal.aborted) {
                            yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                            yield { type: "done" };
                            return;
                        }
                        // Use streaming execution for bash tools
                        let result;
                        if (toolCall.function.name === 'bash') {
                            const gen = this.deps.toolHandler.executeToolStreaming(toolCall);
                            let genResult = await gen.next();
                            while (!genResult.done) {
                                // Check abort between stream chunks
                                if (abortController?.signal.aborted) {
                                    await gen.return({ success: false, error: 'Aborted' });
                                    yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                                    yield { type: "done" };
                                    return;
                                }
                                yield {
                                    type: "tool_stream",
                                    toolStreamData: {
                                        toolCallId: toolCall.id,
                                        toolName: toolCall.function.name,
                                        delta: genResult.value,
                                    },
                                };
                                genResult = await gen.next();
                            }
                            result = genResult.value ?? { success: false, error: 'Tool returned no result' };
                        }
                        else {
                            result = await this.executeToolViaLane(toolCall);
                        }
                        // Check abort after tool execution completes
                        if (abortController?.signal.aborted) {
                            yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                            yield { type: "done" };
                            return;
                        }
                        const toolResultEntry = {
                            type: "tool_result",
                            content: result?.success ? result.output || "Success" : result?.error || "Error occurred",
                            timestamp: new Date(),
                            toolCall: toolCall,
                            toolResult: result,
                        };
                        history.push(toolResultEntry);
                        yield { type: "tool_result", toolCall, toolResult: result };
                        // Note: 'name' is required for Gemini API to match functionResponse with functionCall
                        messages.push({
                            role: "tool",
                            content: sanitizeToolResult(result.success ? result.output || "Success" : result.error || "Error"),
                            tool_call_id: toolCall.id,
                            name: toolCall.function.name,
                        });
                    }
                    inputTokens = this.deps.tokenCounter.countMessageTokens(messages);
                    const currentOutputTokens = this.deps.streamingHandler.getTokenCount() || 0;
                    totalOutputTokens += currentOutputTokens;
                    yield { type: "token_count", tokenCount: inputTokens + totalOutputTokens };
                    // Run after_turn middleware (handles cost recording + limit)
                    if (pipeline) {
                        const ctx = this.buildMiddlewareContext(toolRounds, inputTokens, totalOutputTokens, history, messages, true, abortController);
                        const mwResult = await pipeline.runAfterTurn(ctx);
                        if (mwResult.action === 'stop') {
                            if (mwResult.message)
                                yield { type: "content", content: `\n\n${mwResult.message}` };
                            yield { type: "done" };
                            return;
                        }
                        if (mwResult.action === 'warn' && mwResult.message) {
                            yield { type: "content", content: `\n${mwResult.message}\n` };
                        }
                    }
                    else {
                        // Legacy inline cost check when no pipeline
                        this.config.recordSessionCost(inputTokens, totalOutputTokens);
                        if (this.config.isSessionCostLimitReached()) {
                            const sessionCost = this.config.getSessionCost();
                            const sessionCostLimit = this.config.getSessionCostLimit();
                            yield {
                                type: "content",
                                content: `\n\n💸 Session cost limit reached ($${sessionCost.toFixed(2)} / $${sessionCostLimit.toFixed(2)}).`,
                            };
                            yield { type: "done" };
                            return;
                        }
                    }
                }
                else {
                    break;
                }
            }
            if (toolRounds >= maxToolRounds) {
                yield { type: "content", content: "\n\nMaximum tool execution rounds reached." };
            }
            this.config.recordSessionCost(inputTokens, totalOutputTokens);
            if (this.config.isSessionCostLimitReached()) {
                const sessionCost = this.config.getSessionCost();
                const sessionCostLimit = this.config.getSessionCostLimit();
                yield {
                    type: "content",
                    content: `\n\n💸 Session cost limit reached ($${sessionCost.toFixed(2)} / $${sessionCostLimit.toFixed(2)}).`,
                };
            }
            // Process followup/collect messages if any are queued
            const mqEnd = this.deps.messageQueue;
            if (mqEnd?.hasPendingMessages()) {
                const mode = mqEnd.getMode();
                if (mode === 'followup') {
                    const followups = mqEnd.drain();
                    for (const msg of followups) {
                        messages.push({ role: "user", content: msg.content });
                        history.push({ type: "user", content: msg.content, timestamp: msg.timestamp });
                    }
                    // Signal that followup messages need re-processing (caller handles)
                    yield { type: "steer", steer: { content: `${followups.length} followup message(s) queued`, source: 'queue' } };
                }
                else if (mode === 'collect') {
                    const collected = mqEnd.collect();
                    if (collected) {
                        messages.push({ role: "user", content: collected });
                        history.push({ type: "user", content: collected, timestamp: new Date() });
                        yield { type: "steer", steer: { content: collected, source: 'collect' } };
                    }
                }
            }
            yield { type: "done" };
        }
        catch (error) {
            if (abortController?.signal.aborted) {
                yield { type: "content", content: "\n\n[Operation cancelled by user]" };
                yield { type: "done" };
                return;
            }
            const errorMessage = getErrorMessage(error);
            const errorEntry = {
                type: "assistant",
                content: `Sorry, I encountered an error: ${errorMessage}`,
                timestamp: new Date(),
            };
            history.push(errorEntry);
            messages.push({ role: "assistant", content: errorEntry.content });
            yield { type: "content", content: errorEntry.content };
            yield { type: "done" };
        }
    }
}
//# sourceMappingURL=agent-executor.js.map