diff --git a/src/agent/loop-detector.ts b/src/agent/loop-detector.ts index d6f71e4..e04af22 100644 --- a/src/agent/loop-detector.ts +++ b/src/agent/loop-detector.ts @@ -38,7 +38,7 @@ export class LoopDetector { private readonly textOutputs: string[] = [] constructor(config: LoopDetectionConfig = {}) { - this.maxRepeats = config.maxRepeatedToolCalls ?? 3 + this.maxRepeats = config.maxRepetitions ?? 3 const requestedWindow = config.loopDetectionWindow ?? 4 // Window must be >= threshold, otherwise detection can never trigger. this.windowSize = Math.max(requestedWindow, this.maxRepeats) diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 79096bd..456e4c0 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -350,6 +350,10 @@ export class AgentRunner { // Fall through to execute tools, then inject warning. } // 'continue' — do nothing, let the loop proceed normally. + } else { + // No loop detected this turn — agent has recovered, so reset + // the warning state. A future loop gets a fresh warning cycle. + loopWarned = false } } diff --git a/src/types.ts b/src/types.ts index 25893fb..9fd07cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -241,10 +241,10 @@ export interface AgentConfig { /** Configuration for agent loop detection. */ export interface LoopDetectionConfig { /** - * Maximum consecutive times the same tool call (name + args) can repeat - * before detection triggers. Default: `3`. + * Maximum consecutive times the same tool call (name + args) or text + * output can repeat before detection triggers. Default: `3`. */ - readonly maxRepeatedToolCalls?: number + readonly maxRepetitions?: number /** * Number of recent turns to track for repetition analysis. Default: `4`. */ diff --git a/tests/loop-detection.test.ts b/tests/loop-detection.test.ts index 785cff6..01e9198 100644 --- a/tests/loop-detection.test.ts +++ b/tests/loop-detection.test.ts @@ -117,7 +117,7 @@ describe('LoopDetector', () => { }) it('respects custom threshold', () => { - const detector = new LoopDetector({ maxRepeatedToolCalls: 2 }) + const detector = new LoopDetector({ maxRepetitions: 2 }) expect(detector.recordToolCalls([{ name: 'a', input: {} }])).toBeNull() const info = detector.recordToolCalls([{ name: 'a', input: {} }]) expect(info).not.toBeNull() @@ -175,7 +175,7 @@ describe('LoopDetector', () => { describe('window size', () => { it('clamps windowSize to at least maxRepeats', () => { // Window of 2 with threshold 3 is auto-clamped to 3. - const detector = new LoopDetector({ loopDetectionWindow: 2, maxRepeatedToolCalls: 3 }) + const detector = new LoopDetector({ loopDetectionWindow: 2, maxRepetitions: 3 }) detector.recordToolCalls([{ name: 'a', input: {} }]) detector.recordToolCalls([{ name: 'a', input: {} }]) // Third call triggers because window was clamped to 3 @@ -185,7 +185,7 @@ describe('LoopDetector', () => { }) it('works correctly when window >= threshold', () => { - const detector = new LoopDetector({ loopDetectionWindow: 4, maxRepeatedToolCalls: 3 }) + const detector = new LoopDetector({ loopDetectionWindow: 4, maxRepetitions: 3 }) detector.recordToolCalls([{ name: 'a', input: {} }]) detector.recordToolCalls([{ name: 'a', input: {} }]) const info = detector.recordToolCalls([{ name: 'a', input: {} }]) @@ -224,7 +224,7 @@ describe('AgentRunner loop detection', () => { textResponse('done'), ] const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: 'terminate', }) @@ -240,7 +240,7 @@ describe('AgentRunner loop detection', () => { textResponse('done'), ] const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: 'terminate', }) @@ -262,7 +262,7 @@ describe('AgentRunner loop detection', () => { textResponse('done'), ] const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: 'terminate', }) @@ -283,7 +283,7 @@ describe('AgentRunner loop detection', () => { textResponse('done'), ] const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: 'warn', }) @@ -301,7 +301,7 @@ describe('AgentRunner loop detection', () => { ] const callback = vi.fn().mockReturnValue('terminate') const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: callback, }) @@ -320,7 +320,7 @@ describe('AgentRunner loop detection', () => { ] const callback = vi.fn().mockReturnValue('inject') const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: callback, }) @@ -338,7 +338,7 @@ describe('AgentRunner loop detection', () => { ] const callback = vi.fn().mockReturnValue('continue') const runner = buildRunner(responses, { - maxRepeatedToolCalls: 3, + maxRepetitions: 3, onLoopDetected: callback, })