fix: handle unknown/invalid native tool calls to prevent extension freeze (#9834)

This commit is contained in:
Daniel 2025-12-04 22:21:09 -05:00 committed by GitHub
parent 79458600aa
commit 9f111e174b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 397 additions and 26 deletions

View file

@ -0,0 +1,264 @@
// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
import { describe, it, expect, beforeEach, vi } from "vitest"
import { presentAssistantMessage } from "../presentAssistantMessage"
// Mock dependencies
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
describe("presentAssistantMessage - Unknown Tool Handling", () => {
let mockTask: any
beforeEach(() => {
// Create a mock Task with minimal properties needed for testing
mockTask = {
taskId: "test-task-id",
instanceId: "test-instance",
abort: false,
presentAssistantMessageLocked: false,
presentAssistantMessageHasPendingUpdates: false,
currentStreamingContentIndex: 0,
assistantMessageContent: [],
userMessageContent: [],
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
diffEnabled: false,
consecutiveMistakeCount: 0,
clineMessages: [],
api: {
getModel: () => ({ id: "test-model", info: {} }),
},
browserSession: {
closeBrowser: vi.fn().mockResolvedValue(undefined),
},
recordToolUsage: vi.fn(),
recordToolError: vi.fn(),
toolRepetitionDetector: {
check: vi.fn().mockReturnValue({ allowExecution: true }),
},
providerRef: {
deref: () => ({
getState: vi.fn().mockResolvedValue({
mode: "code",
customModes: [],
}),
}),
},
say: vi.fn().mockResolvedValue(undefined),
ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }),
}
})
it("should return error for unknown tool in native protocol", async () => {
// Set up a tool_use block with an unknown tool name and an ID (native protocol)
const toolCallId = "tool_call_unknown_123"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId, // ID indicates native protocol
name: "nonexistent_tool",
params: { some: "param" },
partial: false,
},
]
// Execute presentAssistantMessage
await presentAssistantMessage(mockTask)
// Verify that a tool_result with error was pushed
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
expect(toolResult.tool_use_id).toBe(toolCallId)
// The error is wrapped in JSON by formatResponse.toolError
expect(toolResult.content).toContain("nonexistent_tool")
expect(toolResult.content).toContain("does not exist")
expect(toolResult.content).toContain("error")
// Verify consecutiveMistakeCount was incremented
expect(mockTask.consecutiveMistakeCount).toBe(1)
// Verify recordToolError was called
expect(mockTask.recordToolError).toHaveBeenCalledWith(
"nonexistent_tool",
expect.stringContaining("Unknown tool"),
)
// Verify error message was shown to user (uses i18n key)
expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
})
it("should return error for unknown tool in XML protocol", async () => {
// Set up a tool_use block with an unknown tool name WITHOUT an ID (XML protocol)
mockTask.assistantMessageContent = [
{
type: "tool_use",
// No ID = XML protocol
name: "fake_tool_that_does_not_exist",
params: { param1: "value1" },
partial: false,
},
]
// Execute presentAssistantMessage
await presentAssistantMessage(mockTask)
// For XML protocol, error is pushed as text blocks
const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
// There should be text blocks with error message
expect(textBlocks.length).toBeGreaterThan(0)
const hasErrorMessage = textBlocks.some(
(block: any) =>
block.text?.includes("fake_tool_that_does_not_exist") && block.text?.includes("does not exist"),
)
expect(hasErrorMessage).toBe(true)
// Verify consecutiveMistakeCount was incremented
expect(mockTask.consecutiveMistakeCount).toBe(1)
// Verify recordToolError was called
expect(mockTask.recordToolError).toHaveBeenCalled()
// Verify error message was shown to user (uses i18n key)
expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
})
it("should handle unknown tool without freezing (native protocol)", async () => {
// This test ensures the extension doesn't freeze when an unknown tool is called
const toolCallId = "tool_call_freeze_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId, // Native protocol
name: "this_tool_definitely_does_not_exist",
params: {},
partial: false,
},
]
// The test will timeout if the extension freezes
const timeoutPromise = new Promise<boolean>((_, reject) => {
setTimeout(() => reject(new Error("Test timed out - extension likely froze")), 5000)
})
const resultPromise = presentAssistantMessage(mockTask).then(() => true)
// Race between the function completing and the timeout
const completed = await Promise.race([resultPromise, timeoutPromise])
expect(completed).toBe(true)
// Verify a tool_result was pushed (critical for API not to freeze)
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
})
it("should increment consecutiveMistakeCount for unknown tools", async () => {
// Test with multiple unknown tools to ensure mistake count increments
const toolCallId = "tool_call_mistake_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool_1",
params: {},
partial: false,
},
]
expect(mockTask.consecutiveMistakeCount).toBe(0)
await presentAssistantMessage(mockTask)
expect(mockTask.consecutiveMistakeCount).toBe(1)
})
it("should set userMessageContentReady after handling unknown tool", async () => {
const toolCallId = "tool_call_ready_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool",
params: {},
partial: false,
},
]
mockTask.didCompleteReadingStream = true
mockTask.userMessageContentReady = false
await presentAssistantMessage(mockTask)
// userMessageContentReady should be set after processing
expect(mockTask.userMessageContentReady).toBe(true)
})
it("should still work with didAlreadyUseTool flag for unknown tool", async () => {
const toolCallId = "tool_call_already_used_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool",
params: {},
partial: false,
},
]
mockTask.didAlreadyUseTool = true
await presentAssistantMessage(mockTask)
// When didAlreadyUseTool is true, should send error tool_result
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
expect(toolResult.is_error).toBe(true)
expect(toolResult.content).toContain("was not executed because a tool has already been used")
})
it("should still work with didRejectTool flag for unknown tool", async () => {
const toolCallId = "tool_call_rejected_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool",
params: {},
partial: false,
},
]
mockTask.didRejectTool = true
await presentAssistantMessage(mockTask)
// When didRejectTool is true, should send error tool_result
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
expect(toolResult.is_error).toBe(true)
expect(toolResult.content).toContain("due to user rejecting a previous tool")
})
})

View file

@ -8,6 +8,7 @@ import { TelemetryService } from "@roo-code/telemetry"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
import { Package } from "../../shared/package"
import { t } from "../../i18n"
import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
import { listFilesTool } from "../tools/ListFilesTool"
@ -333,7 +334,11 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say("text", content, undefined, block.partial)
break
}
case "tool_use":
case "tool_use": {
// Fetch state early so it's available for toolDescription and validation
const state = await cline.providerRef.deref()?.getState()
const { mode, customModes, experiments: stateExperiments, apiConfiguration } = state ?? {}
const toolDescription = (): string => {
switch (block.name) {
case "execute_command":
@ -675,30 +680,46 @@ export async function presentAssistantMessage(cline: Task) {
TelemetryService.instance.captureToolUsage(cline.taskId, block.name, toolProtocol)
}
// Validate tool use before execution.
const {
mode,
customModes,
experiments: stateExperiments,
apiConfiguration,
} = (await cline.providerRef.deref()?.getState()) ?? {}
const modelInfo = cline.api.getModel()
const includedTools = modelInfo?.info?.includedTools
// Validate tool use before execution - ONLY for complete (non-partial) blocks.
// Validating partial blocks would cause validation errors to be thrown repeatedly
// during streaming, pushing multiple tool_results for the same tool_use_id and
// potentially causing the stream to appear frozen.
if (!block.partial) {
const modelInfo = cline.api.getModel()
const includedTools = modelInfo?.info?.includedTools
try {
validateToolUse(
block.name as ToolName,
mode ?? defaultModeSlug,
customModes ?? [],
{ apply_diff: cline.diffEnabled },
block.params,
stateExperiments,
includedTools,
)
} catch (error) {
cline.consecutiveMistakeCount++
pushToolResult(formatResponse.toolError(error.message, toolProtocol))
break
try {
validateToolUse(
block.name as ToolName,
mode ?? defaultModeSlug,
customModes ?? [],
{ apply_diff: cline.diffEnabled },
block.params,
stateExperiments,
includedTools,
)
} catch (error) {
cline.consecutiveMistakeCount++
// For validation errors (unknown tool, tool not allowed for mode), we need to:
// 1. Send a tool_result with the error (required for native protocol)
// 2. NOT set didAlreadyUseTool = true (the tool was never executed, just failed validation)
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
// which would cause the extension to appear to hang
const errorContent = formatResponse.toolError(error.message, toolProtocol)
if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
// For native protocol, push tool_result directly without setting didAlreadyUseTool
cline.userMessageContent.push({
type: "tool_result",
tool_use_id: toolCallId,
content: typeof errorContent === "string" ? errorContent : "(validation error)",
is_error: true,
} as Anthropic.ToolResultBlockParam)
} else {
// For XML protocol, use the standard pushToolResult
pushToolResult(errorContent)
}
break
}
}
// Check for identical consecutive tool calls.
@ -995,9 +1016,40 @@ export async function presentAssistantMessage(cline: Task) {
toolProtocol,
})
break
default: {
// Handle unknown/invalid tool names
// This is critical for native protocol where every tool_use MUST have a tool_result
// Note: This case should rarely be reached since validateToolUse now checks for unknown tools
// CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
// If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
// creating a loop that appears to freeze the extension. Only handle complete blocks.
if (block.partial) {
break
}
const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.`
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, errorMessage)
await cline.say("error", t("tools:unknownToolError", { toolName: block.name }))
// Push tool_result directly for native protocol WITHOUT setting didAlreadyUseTool
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
cline.userMessageContent.push({
type: "tool_result",
tool_use_id: toolCallId,
content: formatResponse.toolError(errorMessage, toolProtocol),
is_error: true,
} as Anthropic.ToolResultBlockParam)
} else {
pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
}
break
}
}
break
}
}
// Seeing out of bounds is fine, it means that the next too call is being

View file

@ -167,9 +167,17 @@ describe("mode-validator", () => {
})
describe("validateToolUse", () => {
it("throws error for disallowed tools in architect mode", () => {
it("throws error for unknown/invalid tools", () => {
// Unknown tools should throw with a specific "Unknown tool" error
expect(() => validateToolUse("unknown_tool" as any, "architect", [])).toThrow(
'Tool "unknown_tool" is not allowed in architect mode.',
'Unknown tool "unknown_tool". This tool does not exist.',
)
})
it("throws error for disallowed tools in architect mode", () => {
// execute_command is a valid tool but not allowed in architect mode
expect(() => validateToolUse("execute_command", "architect", [])).toThrow(
'Tool "execute_command" is not allowed in architect mode.',
)
})

View file

@ -1,7 +1,27 @@
import type { ToolName, ModeConfig } from "@roo-code/types"
import { toolNames as validToolNames } from "@roo-code/types"
import { Mode, isToolAllowedForMode } from "../../shared/modes"
/**
* Checks if a tool name is a valid, known tool.
* Note: This does NOT check if the tool is allowed for a specific mode,
* only that the tool actually exists.
*/
export function isValidToolName(toolName: string): toolName is ToolName {
// Check if it's a valid static tool
if ((validToolNames as readonly string[]).includes(toolName)) {
return true
}
// Check if it's a dynamic MCP tool (mcp_serverName_toolName format)
if (toolName.startsWith("mcp_")) {
return true
}
return false
}
export function validateToolUse(
toolName: ToolName,
mode: Mode,
@ -11,6 +31,15 @@ export function validateToolUse(
experiments?: Record<string, boolean>,
includedTools?: string[],
): void {
// First, check if the tool name is actually a valid/known tool
// This catches completely invalid tool names like "edit_file" that don't exist
if (!isValidToolName(toolName)) {
throw new Error(
`Unknown tool "${toolName}". This tool does not exist. Please use one of the available tools: ${validToolNames.join(", ")}.`,
)
}
// Then check if the tool is allowed for the current mode
if (
!isToolAllowedForMode(
toolName,

View file

@ -7,6 +7,7 @@
"imageWithSize": "Fitxer d'imatge ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo ha intentat utilitzar una eina desconeguda: \"{{toolName}}\". Reintentant...",
"codebaseSearch": {
"approval": "Cercant '{{query}}' a la base de codi..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Bilddatei ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo hat versucht, ein unbekanntes Tool zu verwenden: \"{{toolName}}\". Wiederhole Versuch...",
"codebaseSearch": {
"approval": "Suche nach '{{query}}' im Codebase..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Image file ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo tried to use an unknown tool: \"{{toolName}}\". Retrying...",
"codebaseSearch": {
"approval": "Searching for '{{query}}' in codebase..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Archivo de imagen ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo intentó usar una herramienta desconocida: \"{{toolName}}\". Reintentando...",
"codebaseSearch": {
"approval": "Buscando '{{query}}' en la base de código..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Fichier image ({{size}} Ko)"
},
"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.",
"unknownToolError": "Roo a tenté d'utiliser un outil inconnu : \"{{toolName}}\". Nouvelle tentative...",
"codebaseSearch": {
"approval": "Recherche de '{{query}}' dans la base de code..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "छवि फ़ाइल ({{size}} KB)"
},
"toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।",
"unknownToolError": "Roo ने एक अज्ञात उपकरण का उपयोग करने का प्रयास किया: \"{{toolName}}\"। पुनः प्रयास कर रहा है...",
"codebaseSearch": {
"approval": "कोडबेस में '{{query}}' खोज रहा है..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "File gambar ({{size}} KB)"
},
"toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.",
"unknownToolError": "Roo mencoba menggunakan alat yang tidak dikenal: \"{{toolName}}\". Mencoba lagi...",
"codebaseSearch": {
"approval": "Mencari '{{query}}' di codebase..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "File immagine ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo ha provato ad utilizzare uno strumento sconosciuto: \"{{toolName}}\". Nuovo tentativo...",
"codebaseSearch": {
"approval": "Ricerca di '{{query}}' nella base di codice..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "画像ファイル({{size}} KB"
},
"toolRepetitionLimitReached": "Rooが同じ操作{{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。",
"unknownToolError": "Rooが不明なツールを使用しようとしました:「{{toolName}}」。再試行中...",
"codebaseSearch": {
"approval": "コードベースで '{{query}}' を検索中..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "이미지 파일 ({{size}} KB)"
},
"toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.",
"unknownToolError": "Roo가 알 수 없는 도구를 사용하려고 했습니다: \"{{toolName}}\". 다시 시도 중...",
"codebaseSearch": {
"approval": "코드베이스에서 '{{query}}' 검색 중..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Afbeeldingsbestand ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo probeerde een onbekende tool te gebruiken: \"{{toolName}}\". Opnieuw proberen...",
"codebaseSearch": {
"approval": "Zoeken naar '{{query}}' in codebase..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Plik obrazu ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo próbował użyć nieznanego narzędzia: \"{{toolName}}\". Ponowna próba...",
"codebaseSearch": {
"approval": "Wyszukiwanie '{{query}}' w bazie kodu..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Arquivo de imagem ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo tentou usar uma ferramenta desconhecida: \"{{toolName}}\". Tentando novamente...",
"codebaseSearch": {
"approval": "Pesquisando '{{query}}' na base de código..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Файл изображения ({{size}} КБ)"
},
"toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.",
"unknownToolError": "Roo попытался использовать неизвестный инструмент: \"{{toolName}}\". Повторная попытка...",
"codebaseSearch": {
"approval": "Поиск '{{query}}' в кодовой базе..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Görüntü dosyası ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo bilinmeyen bir araç kullanmaya çalıştı: \"{{toolName}}\". Yeniden deneniyor...",
"codebaseSearch": {
"approval": "Kod tabanında '{{query}}' aranıyor..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "Tệp hình ảnh ({{size}} KB)"
},
"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.",
"unknownToolError": "Roo đã cố gắng sử dụng một công cụ không xác định: \"{{toolName}}\". Đang thử lại...",
"codebaseSearch": {
"approval": "Đang tìm kiếm '{{query}}' trong cơ sở mã..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "图片文件 ({{size}} KB)"
},
"toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。",
"unknownToolError": "Roo 尝试使用未知工具:\"{{toolName}}\"。正在重试...",
"codebaseSearch": {
"approval": "正在搜索代码库中的 '{{query}}'..."
},

View file

@ -7,6 +7,7 @@
"imageWithSize": "圖片檔案 ({{size}} KB)"
},
"toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。",
"unknownToolError": "Roo 嘗試使用未知工具:「{{toolName}}」。正在重試...",
"codebaseSearch": {
"approval": "正在搜尋程式碼庫中的「{{query}}」..."
},