fix: handle reasoning-only responses from Gemini 3 Pro Preview

When using Gemini 3 Pro Preview with high reasoning effort and native tool
calling enabled, the model sometimes returns ONLY reasoning content without
any text or tool uses. This caused the "Unexpected API Response: The language
model did not provide any assistant messages" error.

Changes:
- Add reasoningOnlyResponse() helper to responses.ts that prompts the model
  to continue with actionable output after a reasoning-only response
- Add hasReasoning check in Task.ts validation logic (line 3219)
- Handle reasoning-only responses by saving the assistant message with
  reasoning to history and prompting the model to continue with tools/text

This fix allows the task loop to continue instead of failing when the model
produces a reasoning-only response.

Fixes #10422
This commit is contained in:
Roo Code 2025-12-31 17:33:19 +00:00
parent 2068531801
commit 54a840cb70
2 changed files with 53 additions and 2 deletions

View file

@ -74,6 +74,24 @@ ${instructions}
If you have completed the user's task, use the attempt_completion tool.
If you require additional information from the user, use the ask_followup_question tool.
Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task.
(This is an automated message, so do not respond to it conversationally.)`
},
reasoningOnlyResponse: (protocol?: ToolProtocol) => {
const instructions = getToolInstructionsReminder(protocol)
return `[CONTINUE] Your previous response contained only reasoning/thinking without any text content or tool use. While your reasoning process is valuable, you must now provide actionable output.
${instructions}
# Next Steps
Based on your reasoning, please now:
1. If you have completed the user's task, use the attempt_completion tool.
2. If you need to perform an action, use the appropriate tool.
3. If you require additional information from the user, use the ask_followup_question tool.
4. If you need to communicate something to the user, include text content in your response.
(This is an automated message, so do not respond to it conversationally.)`
},

View file

@ -3209,13 +3209,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// tool use since user can exit at any moment and we wouldn't be
// able to save the assistant's response.
// 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",
)
// Check if we have reasoning content (models like Gemini 3 may return ONLY reasoning)
const hasReasoning = reasoningMessage.length > 0
if (hasTextContent || hasToolUses) {
// Reset counter when we get a successful response with content
this.consecutiveNoAssistantMessagesCount = 0
@ -3345,7 +3348,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Add periodic yielding to prevent blocking
await new Promise((resolve) => setImmediate(resolve))
}
continue
} else if (hasReasoning) {
// Handle reasoning-only response (e.g., Gemini 3 with high reasoning effort)
// The model returned reasoning/thinking content but no text or tool uses.
// This is a valid response that needs prompting to continue with actionable output.
// Reset error counters since we got a valid response (just not actionable yet)
this.consecutiveNoAssistantMessagesCount = 0
this.consecutiveNoToolUseCount = 0
// Save the assistant message with reasoning to history (empty content with reasoning attached)
await this.addToApiConversationHistory(
{ role: "assistant", content: [] },
reasoningMessage || undefined,
)
TelemetryService.instance.captureConversationMessage(this.taskId, "assistant")
// Prompt the model to continue with actionable output
// Use the task's locked protocol for consistent tool instructions
this.userMessageContent.push({
type: "text",
text: formatResponse.reasoningOnlyResponse(this._taskToolProtocol ?? "xml"),
})
// Continue the loop with the prompting message
stack.push({
userContent: [...this.userMessageContent],
includeFileDetails: false,
})
continue
} else {
// If there's no assistant_responses, that means we got no text