Skip to content

Commit 0412337

Browse files
committed
fix: add detection of non-continuous tool repeated calls (#5496)
- Enhanced ToolRepetitionDetector to detect non-continuous repetitive patterns (e.g., ABABAB) - Added pattern detection algorithm that identifies repeating sequences of tool calls - Maintains backward compatibility with existing consecutive repetition detection - Added comprehensive tests for pattern detection scenarios - Addresses issue where AI could get stuck in alternating tool call loops Fixes #5496
1 parent 7fe1c0f commit 0412337

File tree

2 files changed

+342
-7
lines changed

2 files changed

+342
-7
lines changed

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ import { ToolUse } from "../../shared/tools"
22
import { t } from "../../i18n"
33

44
/**
5-
* Class for detecting consecutive identical tool calls
6-
* to prevent the AI from getting stuck in a loop.
5+
* Class for detecting both consecutive and non-consecutive repetitive tool call patterns
6+
* to prevent the AI from getting stuck in loops.
77
*/
88
export class ToolRepetitionDetector {
99
private previousToolCallJson: string | null = null
1010
private consecutiveIdenticalToolCallCount: number = 0
1111
private readonly consecutiveIdenticalToolCallLimit: number
1212

13+
// Enhanced pattern detection for non-continuous repetitions
14+
private toolCallHistory: string[] = []
15+
private readonly maxHistorySize: number = 20 // Keep last 20 tool calls for pattern detection
16+
private readonly minPatternLength: number = 2 // Minimum pattern length (e.g., AB)
17+
private readonly maxPatternLength: number = 6 // Maximum pattern length (e.g., ABCDEF)
18+
private readonly minPatternRepetitions: number = 2 // Minimum repetitions to consider it a loop
19+
1320
/**
1421
* Creates a new ToolRepetitionDetector
1522
* @param limit The maximum number of identical consecutive tool calls allowed
@@ -19,7 +26,7 @@ export class ToolRepetitionDetector {
1926
}
2027

2128
/**
22-
* Checks if the current tool call is identical to the previous one
29+
* Checks if the current tool call is identical to the previous one or forms a repetitive pattern
2330
* and determines if execution should be allowed
2431
*
2532
* @param currentToolCallBlock ToolUse object representing the current tool call
@@ -35,19 +42,21 @@ export class ToolRepetitionDetector {
3542
// Serialize the block to a canonical JSON string for comparison
3643
const currentToolCallJson = this.serializeToolUse(currentToolCallBlock)
3744

38-
// Compare with previous tool call
45+
// Add to history for pattern detection
46+
this.addToHistory(currentToolCallJson)
47+
48+
// Check for consecutive repetitions (existing behavior)
3949
if (this.previousToolCallJson === currentToolCallJson) {
4050
this.consecutiveIdenticalToolCallCount++
4151
} else {
4252
this.consecutiveIdenticalToolCallCount = 1 // Start with 1 for the first occurrence
4353
this.previousToolCallJson = currentToolCallJson
4454
}
4555

46-
// Check if limit is reached
56+
// Check if consecutive limit is reached
4757
if (this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit) {
4858
// Reset counters to allow recovery if user guides the AI past this point
49-
this.consecutiveIdenticalToolCallCount = 0
50-
this.previousToolCallJson = null
59+
this.resetState()
5160

5261
// Return result indicating execution should not be allowed
5362
return {
@@ -59,6 +68,21 @@ export class ToolRepetitionDetector {
5968
}
6069
}
6170

71+
// Check for non-consecutive repetitive patterns
72+
const patternDetectionResult = this.detectRepetitivePattern()
73+
if (patternDetectionResult.isRepetitive) {
74+
// Reset state to allow recovery
75+
this.resetState()
76+
77+
return {
78+
allowExecution: false,
79+
askUser: {
80+
messageKey: "mistake_limit_reached",
81+
messageDetail: t("tools:toolRepetitionLimitReached", { toolName: currentToolCallBlock.name }),
82+
},
83+
}
84+
}
85+
6286
// Execution is allowed
6387
return { allowExecution: true }
6488
}
@@ -92,4 +116,106 @@ export class ToolRepetitionDetector {
92116
// Convert to a canonical JSON string
93117
return JSON.stringify(toolObject)
94118
}
119+
120+
/**
121+
* Adds a tool call to the history for pattern detection
122+
* @param toolCallJson Serialized tool call JSON
123+
*/
124+
private addToHistory(toolCallJson: string): void {
125+
this.toolCallHistory.push(toolCallJson)
126+
127+
// Keep history size manageable
128+
if (this.toolCallHistory.length > this.maxHistorySize) {
129+
this.toolCallHistory.shift() // Remove oldest entry
130+
}
131+
}
132+
133+
/**
134+
* Resets the internal state of the detector
135+
*/
136+
private resetState(): void {
137+
this.consecutiveIdenticalToolCallCount = 0
138+
this.previousToolCallJson = null
139+
// Clear history to prevent false positives after reset
140+
this.toolCallHistory = []
141+
}
142+
143+
/**
144+
* Detects repetitive patterns in the tool call history
145+
* @returns Object indicating if a repetitive pattern was detected
146+
*/
147+
private detectRepetitivePattern(): { isRepetitive: boolean; pattern?: string[] } {
148+
// Need at least enough history to detect a pattern
149+
if (this.toolCallHistory.length < this.minPatternLength * this.minPatternRepetitions) {
150+
return { isRepetitive: false }
151+
}
152+
153+
// Try different pattern lengths, starting from the smallest
154+
for (let patternLength = this.minPatternLength; patternLength <= this.maxPatternLength; patternLength++) {
155+
// Don't check patterns longer than what we can fit with minimum repetitions
156+
if (patternLength * this.minPatternRepetitions > this.toolCallHistory.length) {
157+
break
158+
}
159+
160+
// Check if the last N elements form a repeating pattern
161+
if (this.isRepeatingPattern(patternLength)) {
162+
const pattern = this.toolCallHistory.slice(-patternLength)
163+
return { isRepetitive: true, pattern }
164+
}
165+
}
166+
167+
return { isRepetitive: false }
168+
}
169+
170+
/**
171+
* Checks if the end of the history forms a repeating pattern of the given length
172+
* @param patternLength Length of the pattern to check
173+
* @returns True if a repeating pattern is found
174+
*/
175+
private isRepeatingPattern(patternLength: number): boolean {
176+
const totalLength = patternLength * this.minPatternRepetitions
177+
if (this.toolCallHistory.length < totalLength) {
178+
return false
179+
}
180+
181+
// Get the pattern from the end
182+
const pattern = this.toolCallHistory.slice(-patternLength)
183+
184+
// Check if this pattern repeats the required number of times
185+
for (let i = 1; i < this.minPatternRepetitions; i++) {
186+
const startIndex = this.toolCallHistory.length - (i + 1) * patternLength
187+
const endIndex = this.toolCallHistory.length - i * patternLength
188+
189+
if (startIndex < 0) {
190+
return false
191+
}
192+
193+
const segment = this.toolCallHistory.slice(startIndex, endIndex)
194+
if (!this.arraysEqual(segment, pattern)) {
195+
return false
196+
}
197+
}
198+
199+
return true
200+
}
201+
202+
/**
203+
* Utility method to check if two arrays are equal
204+
* @param arr1 First array
205+
* @param arr2 Second array
206+
* @returns True if arrays are equal
207+
*/
208+
private arraysEqual(arr1: string[], arr2: string[]): boolean {
209+
if (arr1.length !== arr2.length) {
210+
return false
211+
}
212+
213+
for (let i = 0; i < arr1.length; i++) {
214+
if (arr1[i] !== arr2[i]) {
215+
return false
216+
}
217+
}
218+
219+
return true
220+
}
95221
}

0 commit comments

Comments
 (0)