fix(agent): reset loopWarned on recovery and rename maxRepeatedToolCalls to maxRepetitions
- Reset loopWarned flag when the agent stops repeating, so a future loop gets a fresh warning cycle instead of immediate termination - Rename maxRepeatedToolCalls → maxRepetitions since the threshold applies to both tool call and text output repetition detection
This commit is contained in:
parent
56b8cef158
commit
2ecb1f471a
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue