"use strict";
/**
* @file Loop guard ā detects and blocks repeated tool calls.
*
* Two detection layers:
* 1. **Exact match**: Same tool + same params 3Ć within 2 min ā blocked.
* 2. **Error streak**: Same tool fails 4Ć in a row (any params) ā blocked.
* This catches "semantic loops" where the model keeps retrying the same
* tool with slightly different arguments but getting the same error.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.phaseToolCounts = void 0;
exports.resetPhaseToolCounts = resetPhaseToolCounts;
exports.resetLoopTrackers = resetLoopTrackers;
exports.addLoopDetection = addLoopDetection;
const denialTracker_1 = require("./tools/denialTracker");
const workflowCoordinator_1 = require("./tools/workflowCoordinator");
/** Shared phase tool counts ā updated by loopGuard, read by preprocessor. */
exports.phaseToolCounts = { reads: 0, writes: 0, executions: 0, tests: 0 };
/** Reset counts (called on new conversation). */
function resetPhaseToolCounts() {
exports.phaseToolCounts.reads = 0;
exports.phaseToolCounts.writes = 0;
exports.phaseToolCounts.executions = 0;
exports.phaseToolCounts.tests = 0;
}
/** Reset loop trackers. Useful for tests or on explicit "start over" signals. */
function resetLoopTrackers() {
globalTracker.clear();
globalErrorStreaks.clear();
}
const MAX_REPEATS = 3;
const MAX_ERROR_STREAK = 4;
const RESET_MS = 120_000; // 2 minutes
/**
* Module-scoped trackers ā CRITICAL for correctness.
*
* The LM Studio SDK re-invokes toolsProvider frequently (per turn, sometimes
* per tool call) to refresh config. If these Maps lived inside the closure of
* addLoopDetection, each re-invocation would create a fresh tracker and the
* count would never accumulate past 1 ā the loop guard would never fire.
*
* Keeping them module-scoped means repeat calls across ANY instance of the
* wrapped tools accumulate in the same counter, so 3 identical calls always
* trip the guard, regardless of how often the provider is re-run.
*/
const globalTracker = new Map();
const globalErrorStreaks = new Map();
/**
* Mutates each tool's `implementation` to add loop detection.
* Uses module-scoped trackers so counts persist across toolsProvider re-invocations.
*/
function addLoopDetection(tools) {
const tracker = globalTracker;
const errorStreaks = globalErrorStreaks;
for (const t of tools) {
// Only wrap FunctionTool (has `implementation` property)
if (!("implementation" in t) || typeof t.implementation !== "function")
continue;
const ft = t;
const original = ft.implementation;
ft.implementation = async (params, ctx) => {
const key = `${ft.name}:${JSON.stringify(params)}`;
const now = Date.now();
// āā Layer 1: Exact param match āā
const entry = tracker.get(key);
if (entry && now - entry.lastAt < RESET_MS) {
entry.count++;
entry.lastAt = now;
if (entry.count >= MAX_REPEATS) {
const count = entry.count;
tracker.delete(key);
return (`ā ļø LOOP DETECTED: You called "${ft.name}" ${count} times with identical ` +
`parameters. This is not productive. STOP calling tools and respond to the ` +
`user with what you have learned so far. If you are stuck, explain why and ` +
`ask the user for guidance.`);
}
}
else {
tracker.set(key, { count: 1, lastAt: now });
}
// āā Layer 2: Error streak check (before executing) āā
const streak = errorStreaks.get(ft.name);
if (streak && now - streak.lastAt < RESET_MS && streak.count >= MAX_ERROR_STREAK) {
errorStreaks.delete(ft.name);
const hintLine = streak.lastHint
? `\n\nš” RECOVERY HINT: ${streak.lastHint}`
: "";
return (`ā ļø ERROR LOOP DETECTED: "${ft.name}" has failed ${streak.count} consecutive times. ` +
`Last error: ${streak.lastError.substring(0, 200)}` +
hintLine +
`\n\nYou are stuck in a retry loop. STOP and try a completely different approach, ` +
`or ask the user for help. Do NOT call "${ft.name}" again with similar arguments.`);
}
// Cleanup stale entries (>2min old)
for (const [k, v] of tracker) {
if (now - v.lastAt > RESET_MS)
tracker.delete(k);
}
for (const [k, v] of errorStreaks) {
if (now - v.lastAt > RESET_MS)
errorStreaks.delete(k);
}
// āā Execute the tool āā
const result = await original(params, ctx);
// āā Track errors for streak detection āā
const isError = isErrorResult(result);
if (isError) {
const errorMsg = extractErrorMessage(result);
const errorHint = extractHint(result);
const errorCode = extractErrorCode(result);
const existing = errorStreaks.get(ft.name);
if (existing && now - existing.lastAt < RESET_MS) {
existing.count++;
existing.lastAt = now;
existing.lastError = errorMsg;
existing.lastHint = errorHint;
existing.lastCode = errorCode;
}
else {
errorStreaks.set(ft.name, { count: 1, lastAt: now, lastError: errorMsg, lastHint: errorHint, lastCode: errorCode });
}
// āā Layer 3: Denial tracking for security/permission errors āā
if (errorCode >= 300 && errorCode < 400) {
denialTracker_1.denialTracker.record(ft.name, errorCode, errorMsg);
const repeatWarn = denialTracker_1.denialTracker.getRepeatWarning(ft.name, errorCode);
if (repeatWarn) {
return { ...result, _denial_warning: repeatWarn };
}
}
}
else {
// Success resets the error streak
errorStreaks.delete(ft.name);
// āā Phase tracking: count successful tool calls by category āā
const category = (0, workflowCoordinator_1.classifyTool)(ft.name);
if (category)
exports.phaseToolCounts[category]++;
}
return result;
};
}
}
/** Detect error results from tool returns (structured errors only). */
function isErrorResult(result) {
if (result && typeof result === "object") {
// Structured error from toolError() ā has both fields
if ("error" in result && "errorCode" in result)
return true;
// Legacy error objects (from secondary agent dispatch, etc.)
if ("error" in result && typeof result.error === "string" && !("success" in result))
return true;
}
return false;
}
/** Extract a readable error message from a tool result. */
function extractErrorMessage(result) {
if (typeof result === "string")
return result.substring(0, 300);
if (result && typeof result === "object" && "error" in result) {
return String(result.error).substring(0, 300);
}
return String(result).substring(0, 300);
}
/** Extract the recovery hint from a structured error result. */
function extractHint(result) {
if (result && typeof result === "object" && "hint" in result) {
return String(result.hint);
}
return "";
}
/** Extract the numeric error code from a structured error result. */
function extractErrorCode(result) {
if (result && typeof result === "object" && "errorCode" in result) {
return Number(result.errorCode) || 0;
}
return 0;
}
//# sourceMappingURL=loopGuard.js.map