/**
 * Tool Result Sanitization System
 *
 * OpenClaw-inspired result sanitization for:
 * - Image payload downscaling/recompression
 * - Size limiting for tool results
 * - Provider-specific policies
 * - Orphaned tool_result cleanup
 * - Content block sanitization
 */
import { logger } from '../../utils/logger.js';
/**
 * Default provider policies
 */
export const PROVIDER_POLICIES = {
    grok: {
        provider: 'grok',
        maxResultSize: 100_000,
        maxImageDimension: 2048,
        maxImageSize: 5_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
    },
    openai: {
        provider: 'openai',
        maxResultSize: 100_000,
        maxImageDimension: 2048,
        maxImageSize: 20_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
    },
    anthropic: {
        provider: 'anthropic',
        maxResultSize: 100_000,
        maxImageDimension: 1568,
        maxImageSize: 5_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
    },
    gemini: {
        provider: 'gemini',
        maxResultSize: 100_000,
        maxImageDimension: 3072,
        maxImageSize: 20_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg', 'webp'],
        stripThinking: true,
    },
    mistral: {
        provider: 'mistral',
        maxResultSize: 50_000,
        maxImageDimension: 1024,
        maxImageSize: 5_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg'],
        toolCallIdRequirements: {
            alphanumeric: true,
            minLength: 9,
            maxLength: 9,
        },
    },
    ollama: {
        provider: 'ollama',
        maxResultSize: 50_000,
        maxImageDimension: 1024,
        maxImageSize: 5_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg'],
    },
    lmstudio: {
        provider: 'lmstudio',
        maxResultSize: 50_000,
        maxImageDimension: 1024,
        maxImageSize: 5_000_000,
        supportedImageFormats: ['png', 'jpg', 'jpeg'],
    },
};
// ============================================================================
// Result Sanitizer
// ============================================================================
/**
 * Tool result sanitizer
 */
export class ResultSanitizer {
    policy;
    config;
    constructor(config) {
        this.config = config;
        this.policy = PROVIDER_POLICIES[config.provider];
    }
    /**
     * Sanitize a tool result
     */
    sanitize(result) {
        const appliedSanitizers = [];
        let truncated = false;
        let imageResized = false;
        let toolCallIdModified = false;
        const originalSize = this.calculateSize(result);
        // Sanitize tool call ID
        let toolCallId = result.toolCallId;
        if (this.policy.toolCallIdRequirements) {
            const sanitizedId = this.sanitizeToolCallId(result.toolCallId);
            if (sanitizedId !== result.toolCallId) {
                toolCallId = sanitizedId;
                toolCallIdModified = true;
                appliedSanitizers.push('toolCallId');
            }
        }
        // Sanitize output
        let output = result.output;
        if (output) {
            // Strip ANSI codes
            if (this.config.stripAnsi !== false) {
                const stripped = this.stripAnsi(output);
                if (stripped !== output) {
                    output = stripped;
                    appliedSanitizers.push('stripAnsi');
                }
            }
            // Strip control characters
            if (this.config.stripControlChars !== false) {
                const stripped = this.stripControlChars(output);
                if (stripped !== output) {
                    output = stripped;
                    appliedSanitizers.push('stripControlChars');
                }
            }
            // Apply custom sanitizers
            const allSanitizers = [
                ...(this.policy.customSanitizers || []),
                ...(this.config.customSanitizers || []),
            ];
            for (let i = 0; i < allSanitizers.length; i++) {
                const sanitized = allSanitizers[i](output);
                if (sanitized !== output) {
                    output = sanitized;
                    appliedSanitizers.push(`custom_${i}`);
                }
            }
            // Size limit
            const maxSize = this.config.maxResultSize || this.policy.maxResultSize;
            if (output.length > maxSize) {
                output = this.truncate(output, maxSize);
                truncated = true;
                appliedSanitizers.push('truncate');
            }
        }
        // Sanitize error
        let error = result.error;
        if (error) {
            // Limit error message size
            const maxErrorSize = 5000;
            if (error.length > maxErrorSize) {
                error = this.truncate(error, maxErrorSize);
                truncated = true;
                appliedSanitizers.push('truncateError');
            }
        }
        // Sanitize image
        let imageData = result.imageData;
        let imageFormat = result.imageFormat;
        if (imageData) {
            const sanitizedImage = this.sanitizeImage(imageData, imageFormat);
            if (sanitizedImage.resized || sanitizedImage.recompressed) {
                imageResized = true;
                appliedSanitizers.push('image');
            }
            imageData = sanitizedImage.data;
            imageFormat = sanitizedImage.format;
        }
        const sanitized = {
            toolCallId,
            toolName: result.toolName,
            success: result.success,
            output,
            error,
            imageData,
            imageFormat,
            sanitization: {
                truncated,
                originalSize,
                finalSize: this.calculateSize({
                    ...result,
                    toolCallId,
                    output,
                    error,
                    imageData,
                }),
                imageResized,
                toolCallIdModified,
                appliedSanitizers,
            },
        };
        if (appliedSanitizers.length > 0) {
            logger.debug('Sanitized tool result', {
                toolName: result.toolName,
                appliedSanitizers,
                originalSize,
                finalSize: sanitized.sanitization.finalSize,
            });
        }
        return sanitized;
    }
    /**
     * Sanitize tool call ID according to provider requirements
     */
    sanitizeToolCallId(id) {
        const reqs = this.policy.toolCallIdRequirements;
        if (!reqs)
            return id;
        let sanitized = id;
        // Ensure prefix
        if (reqs.prefix && !sanitized.startsWith(reqs.prefix)) {
            sanitized = reqs.prefix + sanitized;
        }
        // Alphanumeric only
        if (reqs.alphanumeric) {
            sanitized = sanitized.replace(/[^a-zA-Z0-9]/g, '');
        }
        // Length constraints
        if (reqs.minLength && sanitized.length < reqs.minLength) {
            // Pad with random alphanumeric
            while (sanitized.length < reqs.minLength) {
                sanitized += Math.random().toString(36).charAt(2);
            }
        }
        if (reqs.maxLength && sanitized.length > reqs.maxLength) {
            sanitized = sanitized.slice(0, reqs.maxLength);
        }
        return sanitized;
    }
    /**
     * Strip ANSI escape codes
     */
    stripAnsi(text) {
        // eslint-disable-next-line no-control-regex
        return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
    }
    /**
     * Strip control characters (except newlines and tabs)
     */
    stripControlChars(text) {
        // eslint-disable-next-line no-control-regex
        return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
    }
    /**
     * Truncate text with suffix
     */
    truncate(text, maxSize) {
        const suffix = this.config.truncationSuffix || '\n\n[Output truncated...]';
        const availableSize = maxSize - suffix.length;
        if (availableSize <= 0) {
            return suffix;
        }
        // Try to truncate at a line boundary
        let truncated = text.slice(0, availableSize);
        const lastNewline = truncated.lastIndexOf('\n');
        if (lastNewline > availableSize * 0.8) {
            truncated = truncated.slice(0, lastNewline);
        }
        return truncated + suffix;
    }
    /**
     * Sanitize image data
     */
    sanitizeImage(base64Data, format) {
        // For now, just validate format and return as-is
        // Full image processing would require sharp or similar library
        const detectedFormat = format || this.detectImageFormat(base64Data);
        // Check if format is supported
        if (!this.policy.supportedImageFormats.includes(detectedFormat)) {
            logger.warn(`Image format ${detectedFormat} not supported by ${this.policy.provider}`);
        }
        // Check size
        const sizeBytes = (base64Data.length * 3) / 4;
        const needsResize = sizeBytes > this.policy.maxImageSize;
        if (needsResize) {
            logger.warn('Image exceeds size limit, would need resizing', {
                size: sizeBytes,
                maxSize: this.policy.maxImageSize,
            });
        }
        return {
            data: base64Data,
            format: detectedFormat,
            resized: false, // Would be true if we actually resized
            recompressed: false,
        };
    }
    /**
     * Detect image format from base64 data
     */
    detectImageFormat(base64Data) {
        const prefix = base64Data.slice(0, 50);
        if (prefix.includes('/9j/'))
            return 'jpeg';
        if (prefix.includes('iVBORw0KGgo'))
            return 'png';
        if (prefix.includes('R0lGOD'))
            return 'gif';
        if (prefix.includes('UklGR'))
            return 'webp';
        return 'unknown';
    }
    /**
     * Calculate approximate size of result
     */
    calculateSize(result) {
        let size = 0;
        if (result.toolCallId)
            size += result.toolCallId.length;
        if (result.toolName)
            size += result.toolName.length;
        if (result.output)
            size += result.output.length;
        if (result.error)
            size += result.error.length;
        if (result.imageData)
            size += (result.imageData.length * 3) / 4;
        return size;
    }
}
/**
 * Validate and sanitize tool use/result pairing
 * Removes orphaned results and creates synthetic results for missing tool uses
 */
export function sanitizeToolUseResultPairing(toolUses, toolResults) {
    const toolUseIds = new Set(toolUses.map(t => t.id));
    const resultIds = new Set(toolResults.map(r => r.toolCallId));
    // Find orphaned results (no matching tool use)
    const orphanedResults = toolResults
        .filter(r => !toolUseIds.has(r.toolCallId))
        .map(r => r.toolCallId);
    // Find missing results (tool use without result)
    const missingResults = toolUses
        .filter(t => !resultIds.has(t.id))
        .map(t => t.id);
    // Create synthetic results for missing tool uses
    const synthesizedResults = missingResults.map(id => {
        const toolUse = toolUses.find(t => t.id === id);
        return {
            toolCallId: id,
            content: `[Tool ${toolUse.name} result not available]`,
        };
    });
    // Remove orphaned results
    const cleanedResults = toolResults.filter(r => toolUseIds.has(r.toolCallId));
    // Add synthesized results
    const finalResults = [...cleanedResults, ...synthesizedResults];
    const valid = orphanedResults.length === 0 && missingResults.length === 0;
    if (!valid) {
        logger.warn('Tool use/result pairing issues', {
            orphanedResults: orphanedResults.length,
            missingResults: missingResults.length,
        });
    }
    return {
        valid,
        orphanedResults,
        missingResults,
        synthesizedResults,
        cleanedResults: finalResults,
    };
}
// ============================================================================
// Factory Functions
// ============================================================================
/**
 * Create a sanitizer for a specific provider
 */
export function createSanitizer(provider, config) {
    return new ResultSanitizer({
        provider,
        ...config,
    });
}
/**
 * Quick sanitize for a provider
 */
export function sanitizeResult(provider, result) {
    const sanitizer = createSanitizer(provider);
    return sanitizer.sanitize(result);
}
export default ResultSanitizer;
//# sourceMappingURL=result-sanitizer.js.map