fix: prevent Gemini grounding loop in Ask mode

- Add detection for Gemini grounding responses with citations
- Prevent consecutive mistake count increment when grounding is used
- Fix infinite loop issue when Gemini provides search results
- Add comprehensive test coverage for grounding detection

Fixes #6503
This commit is contained in:
Roo Code 2025-07-31 19:22:30 +00:00
parent 74672fafcb
commit df215b1802
2 changed files with 263 additions and 1 deletions

View file

@ -1737,7 +1737,21 @@ export class Task extends EventEmitter<TaskEvents> {
// either use a tool or attempt_completion.
const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use")
if (!didToolUse) {
// Check if this is a Gemini grounding response (contains citations/sources)
// Gemini grounding responses include citations that indicate tool usage (search)
const hasGeminiGrounding =
this.apiConfiguration.apiProvider === "gemini" &&
this.assistantMessageContent.some(
(block) =>
block.type === "text" &&
block.content &&
(block.content.includes("Sources:") ||
block.content.includes("[1]") ||
block.content.includes("[2]") ||
/\[\d+\]\(https?:\/\/[^\)]+\)/.test(block.content)),
)
if (!didToolUse && !hasGeminiGrounding) {
this.userMessageContent.push({ type: "text", text: formatResponse.noToolsUsed() })
this.consecutiveMistakeCount++
}

View file

@ -0,0 +1,248 @@
// npx vitest run src/core/task/__tests__/Task.gemini-grounding.spec.ts
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { Task } from "../Task"
import type { ClineProvider } from "../../webview/ClineProvider"
import type { ProviderSettings } from "@roo-code/types"
describe("Task Gemini Grounding Loop Prevention", () => {
let mockProvider: Partial<ClineProvider>
let mockApiConfiguration: ProviderSettings
beforeEach(() => {
mockProvider = {
context: {
globalStorageUri: { fsPath: "/tmp/test" },
} as any,
getState: vi.fn().mockResolvedValue({
mode: "ask",
customModes: [],
experiments: {},
}),
postStateToWebview: vi.fn(),
updateTaskHistory: vi.fn(),
log: vi.fn(),
}
mockApiConfiguration = {
apiProvider: "gemini",
apiModelId: "gemini-2.5-pro",
enableGrounding: true,
} as ProviderSettings
})
afterEach(() => {
vi.clearAllMocks()
})
it("should not increment mistake count when Gemini provides grounding citations", async () => {
const task = new Task({
provider: mockProvider as ClineProvider,
apiConfiguration: mockApiConfiguration,
task: "Test task",
startTask: false,
})
// Wait for task mode initialization
await task.waitForModeInitialization()
// Simulate Gemini response with grounding citations
task.assistantMessageContent = [
{
type: "text",
content:
"Here's information about Tailwind CSS v4:\n\nTailwind CSS v4 introduces several new features...\n\nSources: [1](https://tailwindcss.com/docs), [2](https://github.com/tailwindlabs/tailwindcss)",
partial: false,
},
]
// Set up the state that would normally be set during streaming
task.userMessageContentReady = true
task.userMessageContent = []
const initialMistakeCount = task.consecutiveMistakeCount
// Simulate the tool usage detection logic
const didToolUse = task.assistantMessageContent.some((block) => block.type === "tool_use")
// Check if this is a Gemini grounding response (contains citations/sources)
const hasGeminiGrounding =
task.apiConfiguration.apiProvider === "gemini" &&
task.assistantMessageContent.some(
(block) =>
block.type === "text" &&
block.content &&
(block.content.includes("Sources:") ||
block.content.includes("[1]") ||
block.content.includes("[2]") ||
/\[\d+\]\(https?:\/\/[^\)]+\)/.test(block.content)),
)
if (!didToolUse && !hasGeminiGrounding) {
task.consecutiveMistakeCount++
}
// Verify that mistake count was NOT incremented due to grounding detection
expect(task.consecutiveMistakeCount).toBe(initialMistakeCount)
expect(hasGeminiGrounding).toBe(true)
expect(didToolUse).toBe(false)
})
it("should increment mistake count when Gemini provides response without grounding or tools", async () => {
const task = new Task({
provider: mockProvider as ClineProvider,
apiConfiguration: mockApiConfiguration,
task: "Test task",
startTask: false,
})
// Wait for task mode initialization
await task.waitForModeInitialization()
// Simulate Gemini response without grounding citations or tools
task.assistantMessageContent = [
{
type: "text",
content: "Here's some general information about web development without any sources.",
partial: false,
},
]
// Set up the state that would normally be set during streaming
task.userMessageContentReady = true
task.userMessageContent = []
const initialMistakeCount = task.consecutiveMistakeCount
// Simulate the tool usage detection logic
const didToolUse = task.assistantMessageContent.some((block) => block.type === "tool_use")
// Check if this is a Gemini grounding response (contains citations/sources)
const hasGeminiGrounding =
task.apiConfiguration.apiProvider === "gemini" &&
task.assistantMessageContent.some(
(block) =>
block.type === "text" &&
block.content &&
(block.content.includes("Sources:") ||
block.content.includes("[1]") ||
block.content.includes("[2]") ||
/\[\d+\]\(https?:\/\/[^\)]+\)/.test(block.content)),
)
if (!didToolUse && !hasGeminiGrounding) {
task.consecutiveMistakeCount++
}
// Verify that mistake count WAS incremented since no grounding was detected
expect(task.consecutiveMistakeCount).toBe(initialMistakeCount + 1)
expect(hasGeminiGrounding).toBe(false)
expect(didToolUse).toBe(false)
})
it("should detect various grounding citation formats", async () => {
const task = new Task({
provider: mockProvider as ClineProvider,
apiConfiguration: mockApiConfiguration,
task: "Test task",
startTask: false,
})
// Wait for task mode initialization
await task.waitForModeInitialization()
const testCases = [
{
content: "Information here.\n\nSources: [1](https://example.com)",
shouldDetectGrounding: true,
description: "Sources with numbered links",
},
{
content: "Some info [1](https://example.com) and more [2](https://another.com)",
shouldDetectGrounding: true,
description: "Inline numbered citations",
},
{
content: "Text with [1] reference",
shouldDetectGrounding: true,
description: "Simple numbered reference",
},
{
content: "Just regular text without any citations",
shouldDetectGrounding: false,
description: "No citations",
},
{
content: "Text with [abc] but not numbered",
shouldDetectGrounding: false,
description: "Non-numbered brackets",
},
]
for (const testCase of testCases) {
task.assistantMessageContent = [
{
type: "text",
content: testCase.content,
partial: false,
},
]
const hasGeminiGrounding =
task.apiConfiguration.apiProvider === "gemini" &&
task.assistantMessageContent.some(
(block) =>
block.type === "text" &&
block.content &&
(block.content.includes("Sources:") ||
block.content.includes("[1]") ||
block.content.includes("[2]") ||
/\[\d+\]\(https?:\/\/[^\)]+\)/.test(block.content)),
)
expect(hasGeminiGrounding, `Failed for case: ${testCase.description}`).toBe(testCase.shouldDetectGrounding)
}
})
it("should only apply grounding detection for Gemini provider", async () => {
// Test with non-Gemini provider
const nonGeminiConfig: ProviderSettings = {
apiProvider: "anthropic",
apiModelId: "claude-3-5-sonnet-20241022",
} as ProviderSettings
const task = new Task({
provider: mockProvider as ClineProvider,
apiConfiguration: nonGeminiConfig,
task: "Test task",
startTask: false,
})
// Wait for task mode initialization
await task.waitForModeInitialization()
// Simulate response with citation-like content from non-Gemini provider
task.assistantMessageContent = [
{
type: "text",
content: "Here's information.\n\nSources: [1](https://example.com)",
partial: false,
},
]
const hasGeminiGrounding =
task.apiConfiguration.apiProvider === "gemini" &&
task.assistantMessageContent.some(
(block) =>
block.type === "text" &&
block.content &&
(block.content.includes("Sources:") ||
block.content.includes("[1]") ||
block.content.includes("[2]") ||
/\[\d+\]\(https?:\/\/[^\)]+\)/.test(block.content)),
)
// Should not detect grounding for non-Gemini providers
expect(hasGeminiGrounding).toBe(false)
})
})