import { createHash } from "node:crypto";
// --- Thresholds ---
const MAX_HISTORY = 30;
const GENERIC_REPEAT_WARN = 3;
const GENERIC_REPEAT_CRITICAL = 5;
const PING_PONG_WARN = 5; // pairs (= 10 calls)
const PING_PONG_CRITICAL = 8; // pairs (= 16 calls)
const NO_PROGRESS_WARN = 3;
const NO_PROGRESS_CRITICAL = 5;
const DEFAULT_CIRCUIT_BREAKER = 25;
const DEFAULT_DIVERSITY_BYPASS = 0;
// --- Helpers ---
function quickHash(input) {
    return createHash("md5").update(input).digest("hex").slice(0, 16);
}
function hashArgs(name, action, args) {
    // Exclude volatile fields that change every call
    const stable = {};
    for (const [k, v] of Object.entries(args)) {
        if (k === "action")
            continue;
        stable[k] = v;
    }
    const keys = Object.keys(stable).sort();
    const serialized = keys.map((k) => `${k}:${JSON.stringify(stable[k])}`).join("|");
    return quickHash(`${name}|${action}|${serialized}`);
}
function hashResult(result) {
    // Hash only the first 2000 chars to avoid hashing huge payloads
    return quickHash(result.slice(0, 2000));
}
const LOOP_WARNING_GUIDANCE = "change approach: scroll farther, refresh the snapshot, switch selectors, " +
    "skip the stale candidate, or use a different extraction path. For open-ended " +
    "goals, continue until the target is found, the source is genuinely exhausted, " +
    "or a real blocker appears.";
/**
 * Per-protocol detector tuning. Claude keeps the original thresholds —
 * its >100-char text habit is the existing baseline. OpenAI Chat
 * Completions returns empty `content` next to tool_calls most of the
 * time (observed ~89% empty vs Claude's ~48% on dipcoin002), so it
 * needs (a) a slightly higher cap and (b) diversity bypass to avoid
 * tripping circuit_breaker while making genuine progress.
 */
export function detectorOptionsForProtocol(protocol) {
    if (protocol === "openai") {
        return {
            circuitBreakerThreshold: 40,
            circuitBreakerDiversityBypass: 10,
        };
    }
    return {};
}
// --- Detector ---
export class ToolLoopDetector {
    history = [];
    totalCalls = 0;
    lastSubstantiveResponse = 0; // call index of last LLM text > 100 chars
    circuitBreakerThreshold;
    circuitBreakerDiversityBypass;
    constructor(options = {}) {
        this.circuitBreakerThreshold =
            options.circuitBreakerThreshold ?? DEFAULT_CIRCUIT_BREAKER;
        this.circuitBreakerDiversityBypass =
            options.circuitBreakerDiversityBypass ?? DEFAULT_DIVERSITY_BYPASS;
    }
    /**
     * Record a tool call and its result.
     */
    record(name, action, args, result) {
        this.totalCalls++;
        this.history.push({
            signature: hashArgs(name, action, args),
            resultHash: hashResult(result),
            timestamp: Date.now(),
        });
        if (this.history.length > MAX_HISTORY) {
            this.history.shift();
        }
    }
    /**
     * Notify the detector that the LLM produced a substantive text response.
     */
    notifySubstantiveResponse() {
        this.lastSubstantiveResponse = this.totalCalls;
    }
    /**
     * Check for loop patterns. Call this BEFORE each LLM request.
     */
    check() {
        if (this.history.length < 4)
            return { status: "ok" };
        // 1. Circuit breaker: too many calls without a substantive progress
        // signal. The diversity bypass treats varied tool calls as progress —
        // important for protocols like OpenAI Chat Completions where the
        // assistant routinely returns empty `content` next to its tool_calls
        // and would otherwise hit the cap while making genuine progress.
        const callsSinceSubstantive = this.totalCalls - this.lastSubstantiveResponse;
        if (callsSinceSubstantive >= this.circuitBreakerThreshold) {
            const bypass = this.circuitBreakerDiversityBypass > 0 &&
                this.uniqueSignatureCount() >= this.circuitBreakerDiversityBypass;
            if (!bypass) {
                return {
                    status: "critical",
                    pattern: "circuit_breaker",
                    count: callsSinceSubstantive,
                    message: `CRITICAL: You have made ${callsSinceSubstantive} consecutive tool calls without producing a substantive response. ` +
                        `Stop calling tools and provide a final answer with what you have collected so far.`,
                };
            }
        }
        // 2. Generic repeat: same signature repeated consecutively
        const repeatCount = this.consecutiveRepeatCount();
        if (repeatCount >= GENERIC_REPEAT_CRITICAL) {
            return {
                status: "critical",
                pattern: "generic_repeat",
                count: repeatCount,
                message: `CRITICAL: You called the same tool with identical arguments ${repeatCount} times in a row. ` +
                    `This is a stuck loop. Stop and provide your answer with the data you have.`,
            };
        }
        if (repeatCount >= GENERIC_REPEAT_WARN) {
            return {
                status: "warning",
                pattern: "generic_repeat",
                count: repeatCount,
                message: `WARNING: You have repeated the same tool call ${repeatCount} times. ` +
                    LOOP_WARNING_GUIDANCE,
            };
        }
        // 3. No-progress: same signature yields same result multiple times
        const noProgressCount = this.noProgressCount();
        if (noProgressCount >= NO_PROGRESS_CRITICAL) {
            return {
                status: "critical",
                pattern: "no_progress",
                count: noProgressCount,
                message: `CRITICAL: The last ${noProgressCount} similar actions produced identical results — no new data. ` +
                    `Stop and provide your answer now.`,
            };
        }
        if (noProgressCount >= NO_PROGRESS_WARN) {
            return {
                status: "warning",
                pattern: "no_progress",
                count: noProgressCount,
                message: `WARNING: Your recent actions are producing the same results — no new information is being gained. ` +
                    LOOP_WARNING_GUIDANCE,
            };
        }
        // 4. Ping-pong: two signatures alternating
        const ppCount = this.pingPongCount();
        if (ppCount >= PING_PONG_CRITICAL) {
            return {
                status: "critical",
                pattern: "ping_pong",
                count: ppCount,
                message: `CRITICAL: You are stuck alternating between the same two actions (${ppCount} pairs). ` +
                    `This is a loop. Stop and provide your answer now.`,
            };
        }
        if (ppCount >= PING_PONG_WARN) {
            return {
                status: "warning",
                pattern: "ping_pong",
                count: ppCount,
                message: `WARNING: You are alternating between the same two actions (${ppCount} pairs). ` +
                    LOOP_WARNING_GUIDANCE,
            };
        }
        return { status: "ok" };
    }
    reset() {
        this.history = [];
        this.totalCalls = 0;
        this.lastSubstantiveResponse = 0;
    }
    // --- Detection helpers ---
    consecutiveRepeatCount() {
        if (this.history.length < 2)
            return 0;
        const latest = this.history[this.history.length - 1].signature;
        let count = 1;
        for (let i = this.history.length - 2; i >= 0; i--) {
            if (this.history[i].signature !== latest)
                break;
            count++;
        }
        return count;
    }
    noProgressCount() {
        if (this.history.length < 2)
            return 0;
        // Group recent calls by signature, check if same sig → same result
        const latest = this.history[this.history.length - 1];
        let count = 0;
        for (let i = this.history.length - 1; i >= 0; i--) {
            const entry = this.history[i];
            if (entry.signature !== latest.signature)
                continue;
            if (entry.resultHash === latest.resultHash) {
                count++;
            }
            else {
                break; // result changed — progress was made at some point
            }
        }
        return count;
    }
    uniqueSignatureCount() {
        const seen = new Set();
        for (const entry of this.history)
            seen.add(entry.signature);
        return seen.size;
    }
    pingPongCount() {
        const len = this.history.length;
        if (len < 4)
            return 0;
        const sigA = this.history[len - 1].signature;
        const sigB = this.history[len - 2].signature;
        if (sigA === sigB)
            return 0; // not alternating
        let pairs = 0;
        for (let i = len - 1; i >= 1; i -= 2) {
            if (this.history[i].signature === sigA &&
                this.history[i - 1].signature === sigB) {
                pairs++;
            }
            else {
                break;
            }
        }
        return pairs;
    }
}
//# sourceMappingURL=tool-loop-detection.js.map