mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-14 23:21:19 +00:00
fix: handle reasoning-only responses from Gemini 3 models
- Recognize reasoning-only responses as valid content - Prevent reasoning-only responses from incrementing mistake counter - Add placeholder text when only reasoning is present - Provide gentle continuation prompts without treating as error - Add tests for reasoning-only response handling Fixes #9519
This commit is contained in:
parent
bf3e4d8f1f
commit
1ce3a2f023
2 changed files with 165 additions and 7 deletions
|
|
@ -2904,13 +2904,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// able to save the assistant's response.
|
||||
let didEndLoop = false
|
||||
|
||||
// Check if we have any content to process (text or tool uses)
|
||||
// Check if we have any content to process (text, tool uses, or reasoning)
|
||||
const hasTextContent = assistantMessage.length > 0
|
||||
const hasToolUses = this.assistantMessageContent.some(
|
||||
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
|
||||
)
|
||||
const hasReasoningContent = reasoningMessage && reasoningMessage.length > 0
|
||||
|
||||
if (hasTextContent || hasToolUses) {
|
||||
if (hasTextContent || hasToolUses || hasReasoningContent) {
|
||||
// Display grounding sources to the user if they exist
|
||||
if (pendingGroundingSources.length > 0) {
|
||||
const citationLinks = pendingGroundingSources.map((source, i) => `[${i + 1}](${source.url})`)
|
||||
|
|
@ -2932,6 +2933,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
})
|
||||
}
|
||||
|
||||
// If we have only reasoning content and no other content, add a placeholder
|
||||
// This ensures the assistant message has some content for the API history
|
||||
if (hasReasoningContent && !hasTextContent && !hasToolUses) {
|
||||
assistantContent.push({
|
||||
type: "text" as const,
|
||||
text: "[Reasoning completed]",
|
||||
})
|
||||
}
|
||||
|
||||
// Add tool_use blocks with their IDs for native protocol
|
||||
// This handles both regular ToolUse and McpToolUse types
|
||||
const toolUseBlocks = this.assistantMessageContent.filter(
|
||||
|
|
@ -3003,11 +3013,28 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
)
|
||||
|
||||
if (!didToolUse) {
|
||||
const modelInfo = this.api.getModel().info
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
|
||||
this.userMessageContent.push({ type: "text", text: formatResponse.noToolsUsed(toolProtocol) })
|
||||
this.consecutiveMistakeCount++
|
||||
// Only count as a mistake if there was no reasoning content either
|
||||
// Reasoning-only responses are valid for Gemini 3 models
|
||||
if (!hasReasoningContent) {
|
||||
const modelInfo = this.api.getModel().info
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: formatResponse.noToolsUsed(toolProtocol),
|
||||
})
|
||||
this.consecutiveMistakeCount++
|
||||
} else {
|
||||
// For reasoning-only responses, provide a gentle continuation prompt
|
||||
// without incrementing the mistake counter
|
||||
const modelInfo = this.api.getModel().info
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: "I see you've been thinking about this. Please continue with the task by using the appropriate tools or providing your response.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Push to stack if there's content OR if we're paused waiting for a subtask.
|
||||
|
|
|
|||
|
|
@ -1847,6 +1847,137 @@ describe("Cline", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("Reasoning-only response handling", () => {
|
||||
let mockProvider: any
|
||||
let mockApiConfig: ProviderSettings
|
||||
|
||||
beforeEach(() => {
|
||||
if (!TelemetryService.hasInstance()) {
|
||||
TelemetryService.createInstance([])
|
||||
}
|
||||
|
||||
const storageUri = { fsPath: path.join(os.tmpdir(), "test-storage") }
|
||||
const ctx = {
|
||||
globalState: {
|
||||
get: vi.fn().mockImplementation((_key: keyof GlobalState) => undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
globalStorageUri: storageUri,
|
||||
workspaceState: {
|
||||
get: vi.fn().mockImplementation((_key) => undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
secrets: {
|
||||
get: vi.fn().mockResolvedValue(undefined),
|
||||
store: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
extensionUri: { fsPath: "/mock/extension/path" },
|
||||
extension: { packageJSON: { version: "1.0.0" } },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
const output = {
|
||||
appendLine: vi.fn(),
|
||||
append: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
show: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
}
|
||||
|
||||
mockProvider = new ClineProvider(ctx, output as any, "sidebar", new ContextProxy(ctx)) as any
|
||||
mockProvider.postMessageToWebview = vi.fn().mockResolvedValue(undefined)
|
||||
mockProvider.postStateToWebview = vi.fn().mockResolvedValue(undefined)
|
||||
mockProvider.getState = vi.fn().mockResolvedValue({})
|
||||
mockProvider.updateTaskHistory = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
mockApiConfig = {
|
||||
apiProvider: "openrouter",
|
||||
openRouterModelId: "google/gemini-3-pro-preview",
|
||||
apiKey: "test-api-key",
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle reasoning-only responses without treating them as errors", async () => {
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
// Set up initial state
|
||||
task.consecutiveMistakeCount = 0
|
||||
task.assistantMessageContent = []
|
||||
task.userMessageContent = []
|
||||
|
||||
// Manually simulate the stream processing logic with reasoning-only content
|
||||
const reasoningMessage = "I need to think about this problem..."
|
||||
const hasTextContent = false
|
||||
const hasToolUses = false
|
||||
const hasReasoningContent = reasoningMessage.length > 0
|
||||
|
||||
// This simulates the fix logic in Task.ts
|
||||
if (hasTextContent || hasToolUses || hasReasoningContent) {
|
||||
// Simulate what happens when there's only reasoning and no tools were used
|
||||
const didToolUse = false
|
||||
|
||||
if (!didToolUse) {
|
||||
// Only count as a mistake if there was no reasoning content either
|
||||
if (!hasReasoningContent) {
|
||||
task.consecutiveMistakeCount++
|
||||
} else {
|
||||
// For reasoning-only responses, provide a gentle continuation prompt
|
||||
// without incrementing the mistake counter
|
||||
task.userMessageContent.push({
|
||||
type: "text",
|
||||
text: "I see you've been thinking about this. Please continue with the task by using the appropriate tools or providing your response.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the mistake count did not increase
|
||||
expect(task.consecutiveMistakeCount).toBe(0)
|
||||
|
||||
// Verify a continuation prompt was added
|
||||
expect(task.userMessageContent).toHaveLength(1)
|
||||
expect(task.userMessageContent[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("Please continue with the task"),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("should count as mistake when there is no content at all", async () => {
|
||||
const task = new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
// Simulate scenario with no content at all
|
||||
const reasoningMessage = ""
|
||||
const hasTextContent = false
|
||||
const hasToolUses = false
|
||||
const hasReasoningContent = false
|
||||
|
||||
task.assistantMessageContent = []
|
||||
task.userMessageContent = []
|
||||
task.consecutiveMistakeCount = 0
|
||||
|
||||
// This simulates the logic when there's no content
|
||||
if (!hasTextContent && !hasToolUses && !hasReasoningContent) {
|
||||
// This should be treated as an error since there's no content at all
|
||||
task.consecutiveMistakeCount++
|
||||
}
|
||||
|
||||
expect(task.consecutiveMistakeCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Queued message processing after condense", () => {
|
||||
function createProvider(): any {
|
||||
const storageUri = { fsPath: path.join(os.tmpdir(), "test-storage") }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue