// tools/output-compressor.ts — SPEC-455: Auto-compress verbose tool outputs
// Intercepts ToolResult content blocks and compresses JSON dumps into concise text.
// Wired into safeWithTelemetry so it applies to ALL tools without per-tool migration.
/** Max lines of JSON before truncation kicks in */
const MAX_JSON_LINES = 20;
/** Min length (chars) to consider a text block worth compressing */
const MIN_COMPRESS_LENGTH = 800;
/** Hard cap (chars) for any content block ≈ 600 tokens */
const HARD_CAP_CHARS = 2400;
/** Max lines kept from a long markdown text block */
const MAX_MARKDOWN_LINES = 40;
/**
 * Detects if a string is a JSON dump (starts with { or [).
 * Quick heuristic — avoids parsing large strings unnecessarily.
 */
function looksLikeJson(text) {
    const trimmed = text.trimStart();
    return trimmed.startsWith('{') || trimmed.startsWith('[');
}
/**
 * Truncates a JSON string to MAX_JSON_LINES, appending a summary.
 * Preserves the first N lines (which contain the most useful info)
 * and adds a truncation notice.
 */
function truncateJson(text) {
    const lines = text.split('\n');
    if (lines.length <= MAX_JSON_LINES) {
        return text;
    }
    const kept = lines.slice(0, MAX_JSON_LINES);
    const omitted = lines.length - MAX_JSON_LINES;
    kept.push(`\n... (${omitted} lines omitted — request verbose:true for full output)`);
    return kept.join('\n');
}
/**
 * Attempts to extract a summary from a parsed JSON object.
 * Looks for common patterns: message, humanSummary, summary, status, count.
 * Returns a concise text summary if possible, null otherwise.
 */
function extractJsonSummary(text) {
    try {
        const parsed = JSON.parse(text);
        // If it has a humanSummary field, prefer that
        if (typeof parsed.humanSummary === 'string') {
            return buildSummary(parsed, parsed.humanSummary);
        }
        // If it has a message field, use that as base
        if (typeof parsed.message === 'string') {
            return buildSummary(parsed, parsed.message);
        }
        // Array results — just report count
        if (Array.isArray(parsed)) {
            return `Result: ${parsed.length} items`;
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Build a concise summary from a parsed JSON response.
 * Extracts key metadata fields alongside the primary message.
 */
function buildSummary(parsed, primaryMessage) {
    const parts = [primaryMessage];
    // Common metadata fields to surface
    const metaFields = [
        ['specId', 'Spec'],
        ['status', 'Status'],
        ['projectId', 'Project'],
        ['count', 'Count'],
        ['score', 'Score'],
        ['overallScore', 'Score'],
    ];
    for (const [field, label] of metaFields) {
        const val = parsed[field];
        if (typeof val === 'string') {
            parts.push(`${label}: ${val}`);
        }
        else if (typeof val === 'number' || typeof val === 'boolean') {
            parts.push(`${label}: ${String(val)}`);
        }
    }
    // If there's a nextAction, surface it
    const next = parsed.nextAction;
    if (next !== null && typeof next === 'object') {
        const step = next.step;
        if (typeof step === 'string') {
            parts.push(`Next: ${step}`);
        }
    }
    return parts.join(' | ');
}
/** Truncate long markdown to MAX_MARKDOWN_LINES with an omission notice. */
function truncateMarkdown(text) {
    const lines = text.split('\n');
    if (lines.length <= MAX_MARKDOWN_LINES) {
        return text;
    }
    const kept = lines.slice(0, MAX_MARKDOWN_LINES);
    const omitted = lines.length - MAX_MARKDOWN_LINES;
    kept.push(`\n... (${String(omitted)} lines omitted — request verbose:true for full output)`);
    return kept.join('\n');
}
/** Hard-cap a text block at HARD_CAP_CHARS chars with a truncation notice. */
function hardCap(text) {
    if (text.length <= HARD_CAP_CHARS) {
        return text;
    }
    const kept = text.slice(0, HARD_CAP_CHARS);
    return `${kept}\n\n... (truncated — ${String(text.length - HARD_CAP_CHARS)} chars omitted)`;
}
/**
 * Compresses a single text content block.
 * Strategy:
 * 1. If short enough, leave it alone
 * 2. If JSON-like, extract humanSummary/message or truncate
 * 3. If long markdown, truncate by lines
 * 4. Always apply hard char cap as safety net
 */
function compressTextBlock(text) {
    // Short texts pass through unchanged
    if (text.length < MIN_COMPRESS_LENGTH) {
        return text;
    }
    if (looksLikeJson(text)) {
        const summary = extractJsonSummary(text);
        if (summary !== null) {
            return hardCap(summary);
        }
        return hardCap(truncateJson(text));
    }
    return hardCap(truncateMarkdown(text));
}
/**
 * Post-processes a ToolResult to compress verbose content blocks.
 * Applied to ALL tool outputs via safeWithTelemetry.
 * Preserves structuredContent untouched — only compresses text blocks.
 */
export function compressToolOutput(result) {
    // Don't compress error results — they need full context for debugging
    if (result.isError === true) {
        return result;
    }
    // Don't compress if explicitly disabled
    if (process.env.PLANU_VERBOSE_OUTPUT === 'true') {
        return result;
    }
    const compressed = result.content.map((block) => ({
        ...block,
        text: compressTextBlock(block.text),
    }));
    return { ...result, content: compressed };
}
//# sourceMappingURL=output-compressor.js.map