Detect tool loops (#3240)

This commit is contained in:
Matt Rubens 2025-05-07 14:49:13 -04:00 committed by GitHub
parent 970bd97668
commit cd423d3a5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 483 additions and 23 deletions

View file

@ -40,6 +40,7 @@ import { ToolParamName, ToolResponse, DiffStrategy } from "../shared/tools"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { BrowserSession } from "../services/browser/BrowserSession"
import { McpHub } from "../services/mcp/McpHub"
import { ToolRepetitionDetector } from "./ToolRepetitionDetector"
import { McpServerManager } from "../services/mcp/McpServerManager"
import { telemetryService } from "../services/telemetry/TelemetryService"
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../services/checkpoints"
@ -165,6 +166,9 @@ export class Cline extends EventEmitter<ClineEvents> {
consecutiveMistakeLimit: number
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
// For tracking identical consecutive tool calls
private toolRepetitionDetector: ToolRepetitionDetector
// Not private since it needs to be accessible by tools.
providerRef: WeakRef<ClineProvider>
private readonly globalStoragePath: string
@ -263,6 +267,7 @@ export class Cline extends EventEmitter<ClineEvents> {
}
this.diffStrategy = new MultiSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
onCreated?.(this)
@ -1857,6 +1862,46 @@ export class Cline extends EventEmitter<ClineEvents> {
break
}
// Check for identical consecutive tool calls
if (!block.partial) {
// Use the detector to check for repetition, passing the ToolUse block directly
const repetitionCheck = this.toolRepetitionDetector.check(block)
// If execution is not allowed, notify user and break
if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
// Handle repetition similar to mistake_limit_reached pattern
const { response, text, images } = await this.ask(
repetitionCheck.askUser.messageKey as ClineAsk,
repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
)
if (response === "messageResponse") {
// Add user feedback to userContent
this.userMessageContent.push(
{
type: "text" as const,
text: `Tool repetition limit reached. User feedback: ${text}`,
},
...formatResponse.imageBlocks(images),
)
// Add user feedback to chat
await this.say("user_feedback", text, images)
// Track tool repetition in telemetry
telemetryService.captureConsecutiveMistakeError(this.taskId) // Using existing telemetry method
}
// Return tool result message about the repetition
pushToolResult(
formatResponse.toolError(
`Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
),
)
break
}
}
switch (block.name) {
case "write_to_file":
await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)

View file

@ -0,0 +1,95 @@
import { ToolUse } from "../shared/tools"
import { t } from "../i18n"
/**
* Class for detecting consecutive identical tool calls
* to prevent the AI from getting stuck in a loop.
*/
export class ToolRepetitionDetector {
private previousToolCallJson: string | null = null
private consecutiveIdenticalToolCallCount: number = 0
private readonly consecutiveIdenticalToolCallLimit: number
/**
* Creates a new ToolRepetitionDetector
* @param limit The maximum number of identical consecutive tool calls allowed
*/
constructor(limit: number = 3) {
this.consecutiveIdenticalToolCallLimit = limit
}
/**
* Checks if the current tool call is identical to the previous one
* and determines if execution should be allowed
*
* @param currentToolCallBlock ToolUse object representing the current tool call
* @returns Object indicating if execution is allowed and a message to show if not
*/
public check(currentToolCallBlock: ToolUse): {
allowExecution: boolean
askUser?: {
messageKey: string
messageDetail: string
}
} {
// Serialize the block to a canonical JSON string for comparison
const currentToolCallJson = this.serializeToolUse(currentToolCallBlock)
// Compare with previous tool call
if (this.previousToolCallJson === currentToolCallJson) {
this.consecutiveIdenticalToolCallCount++
} else {
this.consecutiveIdenticalToolCallCount = 1 // Start with 1 for the first occurrence
this.previousToolCallJson = currentToolCallJson
}
// Check if limit is reached
if (this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit) {
// Reset counters to allow recovery if user guides the AI past this point
this.consecutiveIdenticalToolCallCount = 0
this.previousToolCallJson = null
// Return result indicating execution should not be allowed
return {
allowExecution: false,
askUser: {
messageKey: "mistake_limit_reached",
messageDetail: t("tools:toolRepetitionLimitReached", { toolName: currentToolCallBlock.name }),
},
}
}
// Execution is allowed
return { allowExecution: true }
}
/**
* Serializes a ToolUse object into a canonical JSON string for comparison
*
* @param toolUse The ToolUse object to serialize
* @returns JSON string representation of the tool use with sorted parameter keys
*/
private serializeToolUse(toolUse: ToolUse): string {
// Create a new parameters object with alphabetically sorted keys
const sortedParams: Record<string, unknown> = {}
// Get parameter keys and sort them alphabetically
const sortedKeys = Object.keys(toolUse.params).sort()
// Populate the sorted parameters object in a type-safe way
for (const key of sortedKeys) {
if (Object.prototype.hasOwnProperty.call(toolUse.params, key)) {
sortedParams[key] = toolUse.params[key as keyof typeof toolUse.params]
}
}
// Create the object with the tool name and sorted parameters
const toolObject = {
name: toolUse.name,
parameters: sortedParams,
}
// Convert to a canonical JSON string
return JSON.stringify(toolObject)
}
}

View file

@ -0,0 +1,303 @@
import { ToolRepetitionDetector } from "../ToolRepetitionDetector"
import { ToolUse } from "../../shared/tools"
import { ToolName } from "../../schemas"
// Mock the i18n system
jest.mock("../../i18n", () => ({
t: jest.fn((key, options) => {
// For toolRepetitionLimitReached key, return a message with the tool name
if (key === "tools:toolRepetitionLimitReached" && options?.toolName) {
return `Roo appears to be stuck in a loop, attempting the same action (${options.toolName}) repeatedly. This might indicate a problem with its current strategy.`
}
return key
}),
}))
// Helper function to create a mock ToolUse
function createToolUse(name: string, displayName?: string, params: Record<string, string> = {}): ToolUse {
return {
type: "tool_use",
name: (displayName || name) as ToolName,
params,
partial: false,
}
}
describe("ToolRepetitionDetector", () => {
// ===== Initialization tests =====
describe("initialization", () => {
it("should default to a limit of 3 if no argument provided", () => {
const detector = new ToolRepetitionDetector()
// We'll verify this through behavior in subsequent tests
// First call (counter = 1)
const result1 = detector.check(createToolUse("test", "test-tool"))
expect(result1.allowExecution).toBe(true)
// Second identical call (counter = 2)
const result2 = detector.check(createToolUse("test", "test-tool"))
expect(result2.allowExecution).toBe(true)
// Third identical call (counter = 3) reaches the default limit
const result3 = detector.check(createToolUse("test", "test-tool"))
expect(result3.allowExecution).toBe(false)
})
it("should use the custom limit when provided", () => {
const customLimit = 2
const detector = new ToolRepetitionDetector(customLimit)
// First call (counter = 1)
const result1 = detector.check(createToolUse("test", "test-tool"))
expect(result1.allowExecution).toBe(true)
// Second identical call (counter = 2) reaches the custom limit
const result2 = detector.check(createToolUse("test", "test-tool"))
expect(result2.allowExecution).toBe(false)
})
})
// ===== No Repetition tests =====
describe("no repetition", () => {
it("should allow execution for different tool calls", () => {
const detector = new ToolRepetitionDetector()
const result1 = detector.check(createToolUse("first", "first-tool"))
expect(result1.allowExecution).toBe(true)
expect(result1.askUser).toBeUndefined()
const result2 = detector.check(createToolUse("second", "second-tool"))
expect(result2.allowExecution).toBe(true)
expect(result2.askUser).toBeUndefined()
const result3 = detector.check(createToolUse("third", "third-tool"))
expect(result3.allowExecution).toBe(true)
expect(result3.askUser).toBeUndefined()
})
it("should reset the counter when different tool calls are made", () => {
const detector = new ToolRepetitionDetector(2)
// First call
detector.check(createToolUse("same", "same-tool"))
// Second identical call would reach limit of 2, but we'll make a different call
detector.check(createToolUse("different", "different-tool"))
// Back to the first tool - should be allowed since counter was reset
const result = detector.check(createToolUse("same", "same-tool"))
expect(result.allowExecution).toBe(true)
})
})
// ===== Repetition Below Limit tests =====
describe("repetition below limit", () => {
it("should allow execution when repetition is below limit and block when limit reached", () => {
const detector = new ToolRepetitionDetector(3)
// First call (counter = 1)
const result1 = detector.check(createToolUse("repeat", "repeat-tool"))
expect(result1.allowExecution).toBe(true)
// Second identical call (counter = 2)
const result2 = detector.check(createToolUse("repeat", "repeat-tool"))
expect(result2.allowExecution).toBe(true)
// Third identical call (counter = 3) reaches limit
const result3 = detector.check(createToolUse("repeat", "repeat-tool"))
expect(result3.allowExecution).toBe(false)
})
})
// ===== Repetition Reaches Limit tests =====
describe("repetition reaches limit", () => {
it("should block execution when repetition reaches the limit", () => {
const detector = new ToolRepetitionDetector(3)
// First call (counter = 1)
detector.check(createToolUse("repeat", "repeat-tool"))
// Second identical call (counter = 2)
detector.check(createToolUse("repeat", "repeat-tool"))
// Third identical call (counter = 3) - should reach limit
const result = detector.check(createToolUse("repeat", "repeat-tool"))
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
expect(result.askUser?.messageKey).toBe("mistake_limit_reached")
expect(result.askUser?.messageDetail).toContain("repeat-tool")
})
it("should reset internal state after limit is reached", () => {
const detector = new ToolRepetitionDetector(2)
// Reach the limit
detector.check(createToolUse("repeat", "repeat-tool"))
const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit
expect(limitResult.allowExecution).toBe(false)
// Use a new tool call - should be allowed since state was reset
const result = detector.check(createToolUse("new", "new-tool"))
expect(result.allowExecution).toBe(true)
})
})
// ===== Repetition After Limit (Post-Reset) tests =====
describe("repetition after limit", () => {
it("should allow execution of previously problematic tool after reset", () => {
const detector = new ToolRepetitionDetector(2)
// Reach the limit with a specific tool
detector.check(createToolUse("problem", "problem-tool"))
const limitResult = detector.check(createToolUse("problem", "problem-tool")) // This reaches limit
expect(limitResult.allowExecution).toBe(false)
// The same tool that previously caused problems should now be allowed
const result = detector.check(createToolUse("problem", "problem-tool"))
expect(result.allowExecution).toBe(true)
})
it("should require reaching the limit again after reset", () => {
const detector = new ToolRepetitionDetector(2)
// Reach the limit
detector.check(createToolUse("repeat", "repeat-tool"))
const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit
expect(limitResult.allowExecution).toBe(false)
// First call after reset
detector.check(createToolUse("repeat", "repeat-tool"))
// Second identical call (counter = 2) should reach limit again
const result = detector.check(createToolUse("repeat", "repeat-tool"))
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
})
})
// ===== Tool Name Interpolation tests =====
describe("tool name interpolation", () => {
it("should include tool name in the error message", () => {
const detector = new ToolRepetitionDetector(2)
const toolName = "special-tool-name"
// Reach the limit
detector.check(createToolUse("test", toolName))
const result = detector.check(createToolUse("test", toolName))
expect(result.allowExecution).toBe(false)
expect(result.askUser?.messageDetail).toContain(toolName)
})
})
// ===== Edge Cases =====
describe("edge cases", () => {
it("should handle empty tool call", () => {
const detector = new ToolRepetitionDetector(2)
// Create an empty tool call - a tool with no parameters
// Use the empty tool directly in the check calls
detector.check(createToolUse("empty-tool", "empty-tool"))
const result = detector.check(createToolUse("empty-tool"))
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
})
it("should handle different tool names with identical serialized JSON", () => {
const detector = new ToolRepetitionDetector(2)
// First, call with tool-name-1 twice to set up the counter
const toolUse1 = createToolUse("tool-name-1", "tool-name-1", { param: "value" })
detector.check(toolUse1)
// Create a tool that will serialize to the same JSON as toolUse1
// We need to mock the serializeToolUse method to return the same value
const toolUse2 = createToolUse("tool-name-2", "tool-name-2", { param: "value" })
// Override the private method to force identical serialization
const originalSerialize = (detector as any).serializeToolUse
;(detector as any).serializeToolUse = (tool: ToolUse) => {
// Use string comparison for the name since it's technically an enum
if (String(tool.name) === "tool-name-2") {
return (detector as any).serializeToolUse(toolUse1) // Return the same JSON as toolUse1
}
return originalSerialize(tool)
}
// This should detect as a repetition now
const result = detector.check(toolUse2)
// Restore the original method
;(detector as any).serializeToolUse = originalSerialize
// Since we're directly manipulating the internal state for testing,
// we still expect it to consider this a repetition
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
})
it("should treat tools with same parameters in different order as identical", () => {
const detector = new ToolRepetitionDetector(2)
// First call with parameters in one order
const toolUse1 = createToolUse("same-tool", "same-tool", { a: "1", b: "2", c: "3" })
detector.check(toolUse1)
// Create tool with same parameters but in different order
const toolUse2 = createToolUse("same-tool", "same-tool", { c: "3", a: "1", b: "2" })
// This should still detect as a repetition due to canonical JSON with sorted keys
const result = detector.check(toolUse2)
// Since parameters are sorted alphabetically in the serialized JSON,
// these should be considered identical
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
})
})
// ===== Explicit Nth Call Blocking tests =====
describe("explicit Nth call blocking behavior", () => {
it("should block on the 1st call for limit 1", () => {
const detector = new ToolRepetitionDetector(1)
// First call (counter = 1) should be blocked
const result = detector.check(createToolUse("tool", "tool-name"))
expect(result.allowExecution).toBe(false)
expect(result.askUser).toBeDefined()
})
it("should block on the 2nd call for limit 2", () => {
const detector = new ToolRepetitionDetector(2)
// First call (counter = 1)
const result1 = detector.check(createToolUse("tool", "tool-name"))
expect(result1.allowExecution).toBe(true)
// Second call (counter = 2) should be blocked
const result2 = detector.check(createToolUse("tool", "tool-name"))
expect(result2.allowExecution).toBe(false)
expect(result2.askUser).toBeDefined()
})
it("should block on the 3rd call for limit 3 (default)", () => {
const detector = new ToolRepetitionDetector(3)
// First call (counter = 1)
const result1 = detector.check(createToolUse("tool", "tool-name"))
expect(result1.allowExecution).toBe(true)
// Second call (counter = 2)
const result2 = detector.check(createToolUse("tool", "tool-name"))
expect(result2.allowExecution).toBe(true)
// Third call (counter = 3) should be blocked
const result3 = detector.check(createToolUse("tool", "tool-name"))
expect(result3.allowExecution).toBe(false)
expect(result3.askUser).toBeDefined()
})
})
})

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (línies 1-{{end}})",
"definitionsOnly": " (només definicions)",
"maxLines": " (màxim {{max}} línies)"
}
},
"toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (Zeilen 1-{{end}})",
"definitionsOnly": " (nur Definitionen)",
"maxLines": " (maximal {{max}} Zeilen)"
}
},
"toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (lines 1-{{end}})",
"definitionsOnly": " (definitions only)",
"maxLines": " (max {{max}} lines)"
}
},
"toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (líneas 1-{{end}})",
"definitionsOnly": " (solo definiciones)",
"maxLines": " (máximo {{max}} líneas)"
}
},
"toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (lignes 1-{{end}})",
"definitionsOnly": " (définitions uniquement)",
"maxLines": " (max {{max}} lignes)"
}
},
"toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (पंक्तियाँ 1-{{end}})",
"definitionsOnly": " (केवल परिभाषाएँ)",
"maxLines": " (अधिकतम {{max}} पंक्तियाँ)"
}
},
"toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।"
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (righe 1-{{end}})",
"definitionsOnly": " (solo definizioni)",
"maxLines": " (max {{max}} righe)"
}
},
"toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (1-{{end}}行目)",
"definitionsOnly": " (定義のみ)",
"maxLines": " (最大{{max}}行)"
}
},
"toolRepetitionLimitReached": "Rooが同じ操作{{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。"
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (1-{{end}}행)",
"definitionsOnly": " (정의만)",
"maxLines": " (최대 {{max}}행)"
}
},
"toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요."
}

View file

@ -1,9 +1,10 @@
{
"readFile": {
"linesRange": " (regels {{start}}-{{end}})",
"linesFromToEnd": " (regels {{start}}-einde)",
"linesFromStartTo": " (regels 1-{{end}})",
"definitionsOnly": " (alleen definities)",
"maxLines": " (max {{max}} regels)"
}
"readFile": {
"linesRange": " (regels {{start}}-{{end}})",
"linesFromToEnd": " (regels {{start}}-einde)",
"linesFromStartTo": " (regels 1-{{end}})",
"definitionsOnly": " (alleen definities)",
"maxLines": " (max {{max}} regels)"
},
"toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (linie 1-{{end}})",
"definitionsOnly": " (tylko definicje)",
"maxLines": " (maks. {{max}} linii)"
}
},
"toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (linhas 1-{{end}})",
"definitionsOnly": " (apenas definições)",
"maxLines": " (máx. {{max}} linhas)"
}
},
"toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (строки 1-{{end}})",
"definitionsOnly": " (только определения)",
"maxLines": " (макс. {{max}} строк)"
}
},
"toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (satır 1-{{end}})",
"definitionsOnly": " (sadece tanımlar)",
"maxLines": " (maks. {{max}} satır)"
}
},
"toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (dòng 1-{{end}})",
"definitionsOnly": " (chỉ định nghĩa)",
"maxLines": " (tối đa {{max}} dòng)"
}
},
"toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác."
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (第 1-{{end}} 行)",
"definitionsOnly": " (仅定义)",
"maxLines": " (最多 {{max}} 行)"
}
},
"toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。"
}

View file

@ -5,5 +5,6 @@
"linesFromStartTo": " (第 1-{{end}} 行)",
"definitionsOnly": " (僅定義)",
"maxLines": " (最多 {{max}} 行)"
}
},
"toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。"
}