mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add automatic temperature reduction and retry for tool failures
- Add temperature error detection utility to identify temperature-related failures - Add new "temperature_tool_error" ClineAsk type for UI interaction - Modify writeToFileTool and applyDiffTool to detect temperature errors - Add UI handler in ChatView to show temperature reduction option - Implement retry mechanism that reduces temperature to 0.2 and retries - Add comprehensive unit and integration tests Fixes #6156
This commit is contained in:
parent
1e17b3b3bb
commit
17a378aff3
9 changed files with 733 additions and 8 deletions
|
|
@ -24,6 +24,7 @@ import { z } from "zod"
|
|||
* - `browser_action_launch`: Permission to open or interact with a browser
|
||||
* - `use_mcp_server`: Permission to use Model Context Protocol (MCP) server functionality
|
||||
* - `auto_approval_max_req_reached`: Auto-approval limit has been reached, manual approval required
|
||||
* - `temperature_tool_error`: Tool failed due to high temperature setting, asking user to reduce temperature and retry
|
||||
*/
|
||||
export const clineAsks = [
|
||||
"followup",
|
||||
|
|
@ -38,6 +39,7 @@ export const clineAsks = [
|
|||
"browser_action_launch",
|
||||
"use_mcp_server",
|
||||
"auto_approval_max_req_reached",
|
||||
"temperature_tool_error",
|
||||
] as const
|
||||
|
||||
export const clineAskSchema = z.enum(clineAsks)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { formatResponse } from "../prompts/responses"
|
|||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
|
||||
import { unescapeHtmlEntities } from "../../utils/text-normalization"
|
||||
import { isTemperatureRelatedError, getTemperatureErrorMessage } from "./utils/temperatureErrorDetection"
|
||||
|
||||
export async function applyDiffToolLegacy(
|
||||
cline: Task,
|
||||
|
|
@ -133,6 +134,25 @@ export async function applyDiffToolLegacy(
|
|||
await cline.say("diff_error", formattedError)
|
||||
}
|
||||
|
||||
// Check if this is a temperature-related error
|
||||
if (isTemperatureRelatedError("apply_diff", formattedError, cline)) {
|
||||
const currentTemperature = cline.apiConfiguration?.modelTemperature ?? 0.0
|
||||
const temperatureMessage = getTemperatureErrorMessage(currentTemperature)
|
||||
|
||||
// Ask user if they want to reduce temperature and retry
|
||||
const askMessage = JSON.stringify({
|
||||
tool: "apply_diff",
|
||||
path: getReadablePath(cline.cwd, relPath),
|
||||
error: formattedError,
|
||||
temperatureMessage,
|
||||
currentTemperature,
|
||||
})
|
||||
|
||||
await cline.ask("temperature_tool_error", askMessage)
|
||||
cline.recordToolError("apply_diff", formattedError)
|
||||
return
|
||||
}
|
||||
|
||||
cline.recordToolError("apply_diff", formattedError)
|
||||
|
||||
pushToolResult(formattedError)
|
||||
|
|
|
|||
155
src/core/tools/utils/__tests__/temperatureErrorDetection.test.ts
Normal file
155
src/core/tools/utils/__tests__/temperatureErrorDetection.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { isTemperatureRelatedError } from "../temperatureErrorDetection"
|
||||
import type { Task } from "../../../task/Task"
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
describe("temperatureErrorDetection", () => {
|
||||
let mockTask: Partial<Task>
|
||||
let mockApiConfiguration: ProviderSettings
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiConfiguration = {
|
||||
apiProvider: "anthropic",
|
||||
apiModelId: "claude-3-5-sonnet-20241022",
|
||||
modelTemperature: 0.7,
|
||||
} as ProviderSettings
|
||||
|
||||
mockTask = {
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
}
|
||||
})
|
||||
|
||||
describe("isTemperatureRelatedError", () => {
|
||||
it("should return false for non-temperature related errors", () => {
|
||||
const result = isTemperatureRelatedError("write_to_file", "Permission denied", mockTask as Task)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when temperature is already low (0.2 or below)", () => {
|
||||
mockApiConfiguration.modelTemperature = 0.2
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when temperature is undefined (using default)", () => {
|
||||
mockApiConfiguration.modelTemperature = undefined
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for "content appears to be truncated" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for "rest of code unchanged" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"apply_diff",
|
||||
'Error: Found "// rest of code unchanged" in the content',
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for "previous code" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
'Error: Content contains "// ... previous code ..." placeholder',
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for "existing code" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"apply_diff",
|
||||
'Error: Found "// ... existing code ..." in the diff',
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for "keep the rest" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
'Error: Content includes "// keep the rest of the file" comment',
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for "remaining code" error with high temperature', () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"apply_diff",
|
||||
'Error: Found "// ... remaining code ..." in the content',
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle Error objects as well as strings", () => {
|
||||
const error = new Error("content appears to be truncated")
|
||||
const result = isTemperatureRelatedError("write_to_file", error, mockTask as Task)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should be case insensitive when checking error patterns", () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"ERROR: CONTENT APPEARS TO BE TRUNCATED",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when temperature is 0", () => {
|
||||
mockApiConfiguration.modelTemperature = 0
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when temperature is exactly 0.3", () => {
|
||||
mockApiConfiguration.modelTemperature = 0.3
|
||||
const result = isTemperatureRelatedError(
|
||||
"write_to_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should work with apply_diff tool", () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"apply_diff",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for other tools even with temperature error patterns", () => {
|
||||
const result = isTemperatureRelatedError(
|
||||
"read_file",
|
||||
"Error: content appears to be truncated",
|
||||
mockTask as Task,
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
93
src/core/tools/utils/temperatureErrorDetection.ts
Normal file
93
src/core/tools/utils/temperatureErrorDetection.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Utility functions for detecting temperature-related tool errors
|
||||
*/
|
||||
|
||||
import { Task } from "../../task/Task"
|
||||
|
||||
/**
|
||||
* Error patterns that indicate temperature-related issues
|
||||
*/
|
||||
const TEMPERATURE_ERROR_PATTERNS = [
|
||||
// Direct truncation indicators
|
||||
/content appears to be truncated/i,
|
||||
/found comments indicating omitted code/i,
|
||||
/rest of code unchanged/i,
|
||||
/previous code/i,
|
||||
/code omitted/i,
|
||||
/truncated after \d+ lines/i,
|
||||
/keep the rest/i,
|
||||
|
||||
// Common AI placeholder patterns when temperature is high
|
||||
/\/\/\s*\.\.\./,
|
||||
/\/\*\s*\.\.\.\s*\*\//,
|
||||
/\[\s*\.\.\.\s*\]/,
|
||||
/\{\s*\.\.\.\s*\}/,
|
||||
|
||||
// Incomplete content indicators
|
||||
/incomplete file content/i,
|
||||
/partial content/i,
|
||||
/content was cut off/i,
|
||||
]
|
||||
|
||||
/**
|
||||
* Tool names that commonly experience temperature-related failures
|
||||
*/
|
||||
const TEMPERATURE_SENSITIVE_TOOLS = ["write_to_file", "apply_diff"]
|
||||
|
||||
/**
|
||||
* Checks if an error is likely caused by high temperature settings
|
||||
* @param toolName The name of the tool that failed
|
||||
* @param error The error message or Error object
|
||||
* @param task The current task instance to check temperature settings
|
||||
* @returns True if the error appears to be temperature-related
|
||||
*/
|
||||
export function isTemperatureRelatedError(toolName: string, error: string | Error, task: Task): boolean {
|
||||
// Only check for temperature errors on specific tools
|
||||
if (!TEMPERATURE_SENSITIVE_TOOLS.includes(toolName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get current temperature from API configuration
|
||||
const currentTemperature = task.apiConfiguration?.modelTemperature ?? 0.0
|
||||
|
||||
// Only consider it a temperature issue if temperature is above 0.2
|
||||
if (currentTemperature <= 0.2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the user has customized the temperature (not using default)
|
||||
// Most providers default to 0.0 or 1.0, so anything else is likely custom
|
||||
const isCustomTemperature = currentTemperature !== 0.0 && currentTemperature !== 1.0
|
||||
|
||||
if (!isCustomTemperature) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Convert error to string for pattern matching
|
||||
const errorMessage = typeof error === "string" ? error : error.message || ""
|
||||
|
||||
// Check if error matches any temperature-related patterns
|
||||
return TEMPERATURE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user-friendly message explaining the temperature issue
|
||||
* @param currentTemperature The current temperature setting
|
||||
* @returns A message explaining the issue
|
||||
*/
|
||||
export function getTemperatureErrorMessage(currentTemperature: number): string {
|
||||
return (
|
||||
`It looks like the tool failed due to your current temperature setting (${currentTemperature.toFixed(1)}). ` +
|
||||
`Higher temperature values can cause the AI to generate incomplete or malformed outputs. ` +
|
||||
`Reducing the temperature to 0.2 often resolves these issues.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the temperature can be reduced further
|
||||
* @param currentTemperature The current temperature setting
|
||||
* @returns True if temperature can be reduced to 0.2
|
||||
*/
|
||||
export function canReduceTemperature(currentTemperature: number): boolean {
|
||||
return currentTemperature > 0.2
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils"
|
|||
import { detectCodeOmission } from "../../integrations/editor/detect-omission"
|
||||
import { unescapeHtmlEntities } from "../../utils/text-normalization"
|
||||
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
|
||||
import { isTemperatureRelatedError, getTemperatureErrorMessage } from "./utils/temperatureErrorDetection"
|
||||
|
||||
export async function writeToFileTool(
|
||||
cline: Task,
|
||||
|
|
@ -172,13 +173,30 @@ export async function writeToFileTool(
|
|||
if (cline.diffStrategy) {
|
||||
await cline.diffViewProvider.revertChanges()
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`Content appears to be truncated (file has ${
|
||||
newContent.split("\n").length
|
||||
} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
|
||||
),
|
||||
)
|
||||
const errorMessage = `Content appears to be truncated (file has ${
|
||||
newContent.split("\n").length
|
||||
} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`
|
||||
|
||||
// Check if this is a temperature-related error
|
||||
if (isTemperatureRelatedError("write_to_file", errorMessage, cline)) {
|
||||
const currentTemperature = cline.apiConfiguration?.modelTemperature ?? 0.0
|
||||
const temperatureMessage = getTemperatureErrorMessage(currentTemperature)
|
||||
|
||||
// Ask user if they want to reduce temperature and retry
|
||||
const askMessage = JSON.stringify({
|
||||
tool: "write_to_file",
|
||||
path: getReadablePath(cline.cwd, relPath),
|
||||
error: errorMessage,
|
||||
temperatureMessage,
|
||||
currentTemperature,
|
||||
})
|
||||
|
||||
await cline.ask("temperature_tool_error", askMessage)
|
||||
cline.recordToolError("write_to_file", errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult(formatResponse.toolError(errorMessage))
|
||||
return
|
||||
} else {
|
||||
vscode.window
|
||||
|
|
|
|||
359
src/core/webview/__tests__/temperatureErrorRetry.test.ts
Normal file
359
src/core/webview/__tests__/temperatureErrorRetry.test.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { ClineProvider } from "../ClineProvider"
|
||||
import { webviewMessageHandler } from "../webviewMessageHandler"
|
||||
import { Task } from "../../task/Task"
|
||||
import { ProviderSettings } from "@roo-code/types"
|
||||
import { ClineAskResponse, WebviewMessage } from "../../../shared/WebviewMessage"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
showErrorMessage: vi.fn(),
|
||||
showInformationMessage: vi.fn(),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn().mockReturnValue({
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}),
|
||||
},
|
||||
env: {
|
||||
uriScheme: "vscode",
|
||||
language: "en",
|
||||
},
|
||||
ExtensionContext: vi.fn(),
|
||||
OutputChannel: vi.fn(),
|
||||
Uri: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock other dependencies
|
||||
vi.mock("../../api", () => ({
|
||||
buildApiHandler: vi.fn().mockReturnValue({
|
||||
getModel: vi.fn().mockReturnValue({ id: "test-model" }),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../../utils/path", () => ({
|
||||
getWorkspacePath: vi.fn().mockReturnValue("/test/workspace"),
|
||||
}))
|
||||
|
||||
vi.mock("../../i18n", () => ({
|
||||
t: vi.fn((key: string) => key),
|
||||
changeLanguage: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../../services/marketplace", () => ({
|
||||
MarketplaceManager: vi.fn().mockImplementation(() => ({
|
||||
getCurrentItems: vi.fn().mockResolvedValue([]),
|
||||
getInstallationMetadata: vi.fn().mockResolvedValue({ project: {}, global: {} }),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe("Temperature Error Retry Integration", () => {
|
||||
let mockProvider: any
|
||||
let mockTask: any
|
||||
let mockProviderSettingsManager: any
|
||||
let mockContextProxy: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock task
|
||||
mockTask = {
|
||||
taskId: "test-task-123",
|
||||
instanceId: "instance-123",
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
modelTemperature: 0.7,
|
||||
},
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: vi.fn(),
|
||||
overwriteApiConversationHistory: vi.fn(),
|
||||
recursivelyMakeClineRequests: vi.fn(),
|
||||
handleWebviewAskResponse: vi.fn(),
|
||||
}
|
||||
|
||||
// Create mock provider settings manager
|
||||
mockProviderSettingsManager = {
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([{ id: "config-1", name: "default", apiProvider: "anthropic" }]),
|
||||
}
|
||||
|
||||
// Create mock context proxy
|
||||
mockContextProxy = {
|
||||
getValue: vi.fn((key: string) => {
|
||||
const values: Record<string, any> = {
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [{ id: "config-1", name: "default", apiProvider: "anthropic" }],
|
||||
}
|
||||
return values[key]
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
getValues: vi.fn().mockReturnValue({
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [{ id: "config-1", name: "default", apiProvider: "anthropic" }],
|
||||
}),
|
||||
}
|
||||
|
||||
// Create mock provider
|
||||
mockProvider = {
|
||||
getCurrentCline: vi.fn().mockReturnValue(mockTask),
|
||||
getState: vi.fn().mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
modelTemperature: 0.7,
|
||||
},
|
||||
currentApiConfigName: "default",
|
||||
}),
|
||||
upsertProviderProfile: vi.fn().mockImplementation(async () => {
|
||||
// Simulate the internal call to postStateToWebview
|
||||
await mockProvider.postStateToWebview()
|
||||
return "config-1"
|
||||
}),
|
||||
postStateToWebview: vi.fn().mockResolvedValue(undefined),
|
||||
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
log: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should handle temperature error retry flow correctly", async () => {
|
||||
// Setup: Add a temperature error message to the task
|
||||
mockTask.clineMessages = [
|
||||
{
|
||||
ts: Date.now() - 2000,
|
||||
type: "say",
|
||||
say: "assistant",
|
||||
text: "I'll help you with that.",
|
||||
},
|
||||
{
|
||||
ts: Date.now() - 1000,
|
||||
type: "ask",
|
||||
ask: "temperature_tool_error",
|
||||
text: "It looks like the tool failed due to your current temperature setting (0.7).",
|
||||
},
|
||||
]
|
||||
|
||||
// Setup: Add conversation history with user message
|
||||
mockTask.apiConversationHistory = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Please write a function to calculate fibonacci numbers",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "I'll write a fibonacci function for you.",
|
||||
},
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-123",
|
||||
name: "write_to_file",
|
||||
input: {
|
||||
path: "fibonacci.js",
|
||||
content: "function fibonacci(n) {\n // ... rest of code unchanged\n}",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Simulate user clicking "Reduce Temperature to 0.2 & Retry"
|
||||
const message: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked" as ClineAskResponse,
|
||||
}
|
||||
|
||||
// Execute the handler
|
||||
await webviewMessageHandler(mockProvider, message)
|
||||
|
||||
// Verify temperature was updated
|
||||
expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith(
|
||||
"default",
|
||||
expect.objectContaining({
|
||||
apiProvider: "anthropic",
|
||||
modelTemperature: 0.2,
|
||||
}),
|
||||
)
|
||||
|
||||
// Verify messages were removed
|
||||
expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([
|
||||
{
|
||||
ts: expect.any(Number),
|
||||
type: "say",
|
||||
say: "assistant",
|
||||
text: "I'll help you with that.",
|
||||
},
|
||||
])
|
||||
|
||||
// Verify API conversation history was trimmed
|
||||
expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
|
||||
{
|
||||
role: "user",
|
||||
content: "Please write a function to calculate fibonacci numbers",
|
||||
},
|
||||
])
|
||||
|
||||
// Verify the request was retried
|
||||
expect(mockTask.recursivelyMakeClineRequests).toHaveBeenCalledWith([
|
||||
{
|
||||
type: "text",
|
||||
text: "Please write a function to calculate fibonacci numbers",
|
||||
},
|
||||
])
|
||||
|
||||
// Verify state was posted to webview
|
||||
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle cancel button click without making changes", async () => {
|
||||
// Setup: Add a temperature error message
|
||||
mockTask.clineMessages = [
|
||||
{
|
||||
ts: Date.now(),
|
||||
type: "ask",
|
||||
ask: "temperature_tool_error",
|
||||
text: "Temperature error detected",
|
||||
},
|
||||
]
|
||||
|
||||
// Simulate user clicking cancel
|
||||
const message: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked" as ClineAskResponse,
|
||||
}
|
||||
|
||||
// Execute the handler
|
||||
await webviewMessageHandler(mockProvider, message)
|
||||
|
||||
// Verify no temperature update was made
|
||||
expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled()
|
||||
|
||||
// Verify no messages were removed
|
||||
expect(mockTask.overwriteClineMessages).not.toHaveBeenCalled()
|
||||
expect(mockTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
|
||||
|
||||
// Verify no retry was attempted
|
||||
expect(mockTask.recursivelyMakeClineRequests).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle complex content blocks correctly", async () => {
|
||||
// Setup: Add conversation history with complex content blocks
|
||||
mockTask.apiConversationHistory = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Here's an image to analyze:",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: "base64data...",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Please describe what you see.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-456",
|
||||
name: "write_to_file",
|
||||
input: {
|
||||
path: "description.txt",
|
||||
content: "The image shows... [rest of content]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
mockTask.clineMessages = [
|
||||
{
|
||||
ts: Date.now(),
|
||||
type: "ask",
|
||||
ask: "temperature_tool_error",
|
||||
text: "Temperature error",
|
||||
},
|
||||
]
|
||||
|
||||
// Simulate retry
|
||||
const message: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked" as ClineAskResponse,
|
||||
}
|
||||
|
||||
await webviewMessageHandler(mockProvider, message)
|
||||
|
||||
// Verify the content blocks were properly converted
|
||||
expect(mockTask.recursivelyMakeClineRequests).toHaveBeenCalledWith([
|
||||
{
|
||||
type: "text",
|
||||
text: "Here's an image to analyze:",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: "base64data...",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Please describe what you see.",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should not trigger temperature retry for non-temperature errors", async () => {
|
||||
// Setup: Add a different type of error message
|
||||
mockTask.clineMessages = [
|
||||
{
|
||||
ts: Date.now(),
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: "Regular tool error",
|
||||
},
|
||||
]
|
||||
|
||||
// Simulate user response
|
||||
const message: WebviewMessage = {
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked" as ClineAskResponse,
|
||||
}
|
||||
|
||||
// Mock the regular ask response handler
|
||||
mockTask.handleWebviewAskResponse = vi.fn()
|
||||
mockProvider.getCurrentCline.mockReturnValue(mockTask)
|
||||
|
||||
await webviewMessageHandler(mockProvider, message)
|
||||
|
||||
// Verify temperature update was NOT called
|
||||
expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled()
|
||||
|
||||
// Verify regular handler was called instead
|
||||
expect(mockTask.handleWebviewAskResponse).toHaveBeenCalledWith("yesButtonClicked", undefined, undefined)
|
||||
})
|
||||
})
|
||||
|
|
@ -341,6 +341,61 @@ export const webviewMessageHandler = async (
|
|||
await provider.postStateToWebview()
|
||||
break
|
||||
case "askResponse":
|
||||
// Handle temperature error retry with temperature reduction
|
||||
if (message.askResponse === "yesButtonClicked") {
|
||||
const currentCline = provider.getCurrentCline()
|
||||
if (currentCline) {
|
||||
const lastMessage = currentCline.clineMessages.at(-1)
|
||||
if (lastMessage?.ask === "temperature_tool_error") {
|
||||
// User clicked "Reduce Temperature to 0.2 & Retry"
|
||||
// 1. Update temperature to 0.2 for current API profile
|
||||
const { apiConfiguration, currentApiConfigName } = await provider.getState()
|
||||
if (apiConfiguration) {
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
modelTemperature: 0.2,
|
||||
}
|
||||
|
||||
// Update the current configuration using the provider's method
|
||||
await provider.upsertProviderProfile(currentApiConfigName, updatedConfig)
|
||||
}
|
||||
|
||||
// 2. Remove the temperature error message and the failed tool message before it
|
||||
const messageIndex = currentCline.clineMessages.length - 1
|
||||
const apiConversationHistoryIndex = currentCline.apiConversationHistory.length - 1
|
||||
|
||||
// Remove the temperature_tool_error ask message
|
||||
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
|
||||
|
||||
// Remove the last assistant message from API history (the one with the failed tool)
|
||||
if (apiConversationHistoryIndex >= 0) {
|
||||
await currentCline.overwriteApiConversationHistory(
|
||||
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Re-send the last user message to retry with lower temperature
|
||||
const lastUserMessage = currentCline.apiConversationHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "user")
|
||||
|
||||
if (lastUserMessage && lastUserMessage.content) {
|
||||
// Convert content to ContentBlockParam[] format
|
||||
const contentBlocks =
|
||||
typeof lastUserMessage.content === "string"
|
||||
? [{ type: "text" as const, text: lastUserMessage.content }]
|
||||
: lastUserMessage.content
|
||||
|
||||
// Re-initiate the task loop with the last user content
|
||||
await currentCline.recursivelyMakeClineRequests(contentBlocks)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||
break
|
||||
case "autoCondenseContext":
|
||||
|
|
|
|||
|
|
@ -384,6 +384,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setSecondaryButtonText(undefined)
|
||||
setDidClickCancel(false)
|
||||
break
|
||||
case "temperature_tool_error":
|
||||
if (!isPartial) {
|
||||
playSound("notification")
|
||||
}
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("temperature_tool_error")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText(t("chat:temperatureError.reduceAndRetry"))
|
||||
setSecondaryButtonText(t("chat:cancel.title"))
|
||||
break
|
||||
}
|
||||
break
|
||||
case "say":
|
||||
|
|
@ -614,6 +624,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "temperature_tool_error":
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
|
|
@ -669,6 +680,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "temperature_tool_error":
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
|
|
@ -1796,7 +1808,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
: primaryButtonText ===
|
||||
t("chat:proceedWhileRunning.title")
|
||||
? t("chat:proceedWhileRunning.tooltip")
|
||||
: undefined
|
||||
: primaryButtonText ===
|
||||
t(
|
||||
"chat:temperatureError.reduceAndRetry",
|
||||
)
|
||||
? t(
|
||||
"chat:temperatureError.reduceAndRetryTooltip",
|
||||
)
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@
|
|||
"title": "Retry",
|
||||
"tooltip": "Try the operation again"
|
||||
},
|
||||
"temperatureError": {
|
||||
"reduceAndRetry": "Reduce Temperature to 0.2 & Retry",
|
||||
"reduceAndRetryTooltip": "Lower the temperature setting to 0.2 and retry the operation"
|
||||
},
|
||||
"startNewTask": {
|
||||
"title": "Start New Task",
|
||||
"tooltip": "Begin a new task"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue