Skip to content

Commit 91e7432

Browse files
committed
Tool repetition detection
1 parent 94ae448 commit 91e7432

File tree

20 files changed

+483
-23
lines changed

20 files changed

+483
-23
lines changed

src/core/Cline.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { ToolParamName, ToolResponse, DiffStrategy } from "../shared/tools"
4040
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
4141
import { BrowserSession } from "../services/browser/BrowserSession"
4242
import { McpHub } from "../services/mcp/McpHub"
43+
import { ToolRepetitionDetector } from "./ToolRepetitionDetector"
4344
import { McpServerManager } from "../services/mcp/McpServerManager"
4445
import { telemetryService } from "../services/telemetry/TelemetryService"
4546
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../services/checkpoints"
@@ -165,6 +166,9 @@ export class Cline extends EventEmitter<ClineEvents> {
165166
consecutiveMistakeLimit: number
166167
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
167168

169+
// For tracking identical consecutive tool calls
170+
private toolRepetitionDetector: ToolRepetitionDetector
171+
168172
// Not private since it needs to be accessible by tools.
169173
providerRef: WeakRef<ClineProvider>
170174
private readonly globalStoragePath: string
@@ -263,6 +267,7 @@ export class Cline extends EventEmitter<ClineEvents> {
263267
}
264268

265269
this.diffStrategy = new MultiSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
270+
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
266271

267272
onCreated?.(this)
268273

@@ -1857,6 +1862,46 @@ export class Cline extends EventEmitter<ClineEvents> {
18571862
break
18581863
}
18591864

1865+
// Check for identical consecutive tool calls
1866+
if (!block.partial) {
1867+
// Use the detector to check for repetition, passing the ToolUse block directly
1868+
const repetitionCheck = this.toolRepetitionDetector.check(block)
1869+
1870+
// If execution is not allowed, notify user and break
1871+
if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
1872+
// Handle repetition similar to mistake_limit_reached pattern
1873+
const { response, text, images } = await this.ask(
1874+
repetitionCheck.askUser.messageKey as ClineAsk,
1875+
repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
1876+
)
1877+
1878+
if (response === "messageResponse") {
1879+
// Add user feedback to userContent
1880+
this.userMessageContent.push(
1881+
{
1882+
type: "text" as const,
1883+
text: `Tool repetition limit reached. User feedback: ${text}`,
1884+
},
1885+
...formatResponse.imageBlocks(images),
1886+
)
1887+
1888+
// Add user feedback to chat
1889+
await this.say("user_feedback", text, images)
1890+
1891+
// Track tool repetition in telemetry
1892+
telemetryService.captureConsecutiveMistakeError(this.taskId) // Using existing telemetry method
1893+
}
1894+
1895+
// Return tool result message about the repetition
1896+
pushToolResult(
1897+
formatResponse.toolError(
1898+
`Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
1899+
),
1900+
)
1901+
break
1902+
}
1903+
}
1904+
18601905
switch (block.name) {
18611906
case "write_to_file":
18621907
await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)

src/core/ToolRepetitionDetector.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { ToolUse } from "../shared/tools"
2+
import { t } from "../i18n"
3+
4+
/**
5+
* Class for detecting consecutive identical tool calls
6+
* to prevent the AI from getting stuck in a loop.
7+
*/
8+
export class ToolRepetitionDetector {
9+
private previousToolCallJson: string | null = null
10+
private consecutiveIdenticalToolCallCount: number = 0
11+
private readonly consecutiveIdenticalToolCallLimit: number
12+
13+
/**
14+
* Creates a new ToolRepetitionDetector
15+
* @param limit The maximum number of identical consecutive tool calls allowed
16+
*/
17+
constructor(limit: number = 3) {
18+
this.consecutiveIdenticalToolCallLimit = limit
19+
}
20+
21+
/**
22+
* Checks if the current tool call is identical to the previous one
23+
* and determines if execution should be allowed
24+
*
25+
* @param currentToolCallBlock ToolUse object representing the current tool call
26+
* @returns Object indicating if execution is allowed and a message to show if not
27+
*/
28+
public check(currentToolCallBlock: ToolUse): {
29+
allowExecution: boolean
30+
askUser?: {
31+
messageKey: string
32+
messageDetail: string
33+
}
34+
} {
35+
// Serialize the block to a canonical JSON string for comparison
36+
const currentToolCallJson = this.serializeToolUse(currentToolCallBlock)
37+
38+
// Compare with previous tool call
39+
if (this.previousToolCallJson === currentToolCallJson) {
40+
this.consecutiveIdenticalToolCallCount++
41+
} else {
42+
this.consecutiveIdenticalToolCallCount = 1 // Start with 1 for the first occurrence
43+
this.previousToolCallJson = currentToolCallJson
44+
}
45+
46+
// Check if limit is reached
47+
if (this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit) {
48+
// Reset counters to allow recovery if user guides the AI past this point
49+
this.consecutiveIdenticalToolCallCount = 0
50+
this.previousToolCallJson = null
51+
52+
// Return result indicating execution should not be allowed
53+
return {
54+
allowExecution: false,
55+
askUser: {
56+
messageKey: "mistake_limit_reached",
57+
messageDetail: t("tools:toolRepetitionLimitReached", { toolName: currentToolCallBlock.name }),
58+
},
59+
}
60+
}
61+
62+
// Execution is allowed
63+
return { allowExecution: true }
64+
}
65+
66+
/**
67+
* Serializes a ToolUse object into a canonical JSON string for comparison
68+
*
69+
* @param toolUse The ToolUse object to serialize
70+
* @returns JSON string representation of the tool use with sorted parameter keys
71+
*/
72+
private serializeToolUse(toolUse: ToolUse): string {
73+
// Create a new parameters object with alphabetically sorted keys
74+
const sortedParams: Record<string, unknown> = {}
75+
76+
// Get parameter keys and sort them alphabetically
77+
const sortedKeys = Object.keys(toolUse.params).sort()
78+
79+
// Populate the sorted parameters object in a type-safe way
80+
for (const key of sortedKeys) {
81+
if (Object.prototype.hasOwnProperty.call(toolUse.params, key)) {
82+
sortedParams[key] = toolUse.params[key as keyof typeof toolUse.params]
83+
}
84+
}
85+
86+
// Create the object with the tool name and sorted parameters
87+
const toolObject = {
88+
name: toolUse.name,
89+
parameters: sortedParams,
90+
}
91+
92+
// Convert to a canonical JSON string
93+
return JSON.stringify(toolObject)
94+
}
95+
}

0 commit comments

Comments
 (0)