/**
 * Streaming Handler Module
 *
 * Handles message streaming and accumulation for the agent.
 * Implements delta reduction (messageReducer pattern), content sanitization,
 * token counting during streaming, and tool call extraction from content.
 *
 * @module agent/streaming
 */
import { createTokenCounter } from '../../context/token-counter.js';
import { sanitizeLLMOutput, extractCommentaryToolCalls } from '../../utils/sanitize.js';
import { reduceStreamChunk } from './message-reducer.js';
/**
 * StreamingHandler manages the accumulation and processing of streaming
 * responses from LLM APIs. It handles:
 *
 * - Delta reduction: Accumulating incremental updates into a complete message
 * - Content sanitization: Removing LLM control tokens from output
 * - Token counting: Real-time estimation of token usage
 * - Tool call extraction: Detecting tool calls from both native format and commentary patterns
 *
 * @example
 * ```typescript
 * const handler = new StreamingHandler({ sanitizeOutput: true });
 *
 * for await (const chunk of stream) {
 *   const result = handler.accumulateChunk(chunk);
 *   if (result.displayContent) {
 *     console.log(result.displayContent);
 *   }
 *   if (result.hasNewToolCalls) {
 *     console.log('Tool calls:', result.toolCalls);
 *   }
 * }
 *
 * const finalMessage = handler.getAccumulatedMessage();
 * handler.reset();
 * ```
 */
export class StreamingHandler {
    config;
    accumulatedMessage = {};
    accumulatedRawContent = '';
    accumulatedReasoningContent = '';
    tokenCounter = null;
    lastTokenUpdate = 0;
    toolCallsYielded = false;
    /**
     * Creates a new StreamingHandler instance.
     *
     * @param config - Configuration options for the handler
     */
    constructor(config = {}) {
        this.config = {
            sanitizeOutput: config.sanitizeOutput ?? true,
            extractToolCalls: config.extractToolCalls ?? true,
            trackTokens: config.trackTokens ?? true,
            model: config.model ?? 'gpt-4',
            tokenUpdateInterval: config.tokenUpdateInterval ?? 500,
        };
        if (this.config.trackTokens) {
            this.tokenCounter = createTokenCounter(this.config.model);
        }
    }
    /**
     * Processes a streaming chunk and accumulates it into the message.
     *
     * @param chunk - The streaming chunk from the API
     * @returns Processed chunk with display content and metadata
     */
    accumulateChunk(chunk) {
        // Skip chunks without choices
        if (!chunk.choices?.[0]) {
            return {
                displayContent: '',
                rawContent: '',
                hasNewToolCalls: false,
                shouldEmitTokenCount: false,
            };
        }
        // Detect reasoning/thinking content
        const delta = chunk.choices[0].delta;
        const reasoningDelta = delta?.reasoning_content || delta?.reasoning || '';
        if (reasoningDelta) {
            this.accumulatedReasoningContent += reasoningDelta;
        }
        // Accumulate the message using reducer
        this.accumulatedMessage = reduceStreamChunk(this.accumulatedMessage, chunk);
        // Extract content delta
        const rawContentDelta = chunk.choices[0].delta?.content || '';
        this.accumulatedRawContent += rawContentDelta;
        // Sanitize content for display
        const displayContent = this.config.sanitizeOutput
            ? sanitizeLLMOutput(rawContentDelta)
            : rawContentDelta;
        // Check for tool calls
        const toolCalls = this.accumulatedMessage.tool_calls;
        let hasNewToolCalls = false;
        if (!this.toolCallsYielded && Array.isArray(toolCalls) && toolCalls.length > 0) {
            // Check if we have at least one complete tool call with a function name
            const hasCompleteTool = toolCalls.some((tc) => typeof tc === 'object' &&
                tc !== null &&
                'function' in tc &&
                typeof tc.function === 'object' &&
                tc.function !== null &&
                'name' in tc.function);
            if (hasCompleteTool) {
                hasNewToolCalls = true;
                this.toolCallsYielded = true;
            }
        }
        // Calculate token count if enabled
        let tokenCount;
        let shouldEmitTokenCount = false;
        if (this.config.trackTokens && this.tokenCounter && displayContent) {
            const contentTokens = this.tokenCounter.estimateStreamingTokens(this.accumulatedRawContent);
            const toolCallTokens = this.accumulatedMessage.tool_calls
                ? this.tokenCounter.countTokens(JSON.stringify(this.accumulatedMessage.tool_calls))
                : 0;
            tokenCount = contentTokens + toolCallTokens;
            // Check if we should emit a token count update
            const now = Date.now();
            if (now - this.lastTokenUpdate > this.config.tokenUpdateInterval) {
                this.lastTokenUpdate = now;
                shouldEmitTokenCount = true;
            }
        }
        return {
            displayContent,
            rawContent: rawContentDelta,
            hasNewToolCalls,
            toolCalls: hasNewToolCalls ? toolCalls : undefined,
            tokenCount,
            shouldEmitTokenCount,
            reasoningContent: reasoningDelta || undefined,
        };
    }
    /**
     * Extracts tool calls from commentary patterns in the accumulated content.
     * This handles models that don't support native OpenAI tool calls.
     *
     * Supported patterns:
     * - "commentary to=tool_name {...}"
     * - "commentary to=tool_name <|constrain|>json<|message|>{...}"
     * - Direct tool patterns: "web_search {...}"
     *
     * @returns Extracted tool calls and remaining content
     */
    extractToolCalls() {
        if (!this.config.extractToolCalls || !this.accumulatedRawContent) {
            return {
                toolCalls: [],
                remainingContent: this.accumulatedRawContent,
            };
        }
        // Check if we already have native tool calls
        const existingToolCalls = this.accumulatedMessage.tool_calls;
        if (Array.isArray(existingToolCalls) && existingToolCalls.length > 0) {
            return {
                toolCalls: [],
                remainingContent: this.accumulatedRawContent,
            };
        }
        // Extract commentary-style tool calls
        const { toolCalls: extractedCalls, remainingContent } = extractCommentaryToolCalls(this.accumulatedRawContent);
        if (extractedCalls.length === 0) {
            return {
                toolCalls: [],
                remainingContent: this.accumulatedRawContent,
            };
        }
        // Convert extracted calls to OpenAI tool call format
        const convertedCalls = extractedCalls.map((tc, index) => ({
            id: `commentary_${Date.now()}_${index}`,
            type: 'function',
            function: {
                name: tc.name,
                arguments: JSON.stringify(tc.arguments),
            },
        }));
        // Update accumulated message state
        this.accumulatedMessage.tool_calls = convertedCalls;
        this.accumulatedMessage.content = remainingContent;
        this.accumulatedRawContent = remainingContent;
        this.toolCallsYielded = true;
        return {
            toolCalls: convertedCalls,
            remainingContent,
        };
    }
    /**
     * Gets the current accumulated message state.
     *
     * @returns The accumulated message with all processed content and tool calls
     */
    getAccumulatedMessage() {
        return { ...this.accumulatedMessage };
    }
    /**
     * Gets the raw accumulated content before sanitization.
     * Useful for tool call extraction that needs the original patterns.
     *
     * @returns The raw accumulated content string
     */
    getRawContent() {
        return this.accumulatedRawContent;
    }
    /**
     * Gets the sanitized accumulated content.
     *
     * @returns The sanitized content string
     */
    getSanitizedContent() {
        if (this.config.sanitizeOutput) {
            return sanitizeLLMOutput(this.accumulatedRawContent);
        }
        return this.accumulatedRawContent;
    }
    /**
     * Gets the current token count estimate.
     *
     * @returns Token count or undefined if tracking is disabled
     */
    getTokenCount() {
        if (!this.config.trackTokens || !this.tokenCounter) {
            return undefined;
        }
        const contentTokens = this.tokenCounter.estimateStreamingTokens(this.accumulatedRawContent);
        const toolCallTokens = this.accumulatedMessage.tool_calls
            ? this.tokenCounter.countTokens(JSON.stringify(this.accumulatedMessage.tool_calls))
            : 0;
        return contentTokens + toolCallTokens;
    }
    /**
     * Checks if tool calls have been yielded/detected.
     *
     * @returns True if tool calls have been detected and yielded
     */
    hasYieldedToolCalls() {
        return this.toolCallsYielded;
    }
    /**
     * Resets the handler state for a new streaming session.
     * Call this before processing a new response stream.
     */
    reset() {
        this.accumulatedMessage = {};
        this.accumulatedRawContent = '';
        this.accumulatedReasoningContent = '';
        this.lastTokenUpdate = 0;
        this.toolCallsYielded = false;
    }
    /**
     * Disposes of resources used by the handler.
     * Call this when the handler is no longer needed.
     */
    dispose() {
        if (this.tokenCounter) {
            this.tokenCounter.dispose();
            this.tokenCounter = null;
        }
    }
}
// Re-export utility functions for convenience
export { sanitizeLLMOutput, extractCommentaryToolCalls };
//# sourceMappingURL=streaming-handler.js.map