diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 6db345bba2..117cf3e902 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -58,6 +58,7 @@ import { OpenAiHandler } from "../api/providers/openai" import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker" import getFolderSize from "get-folder-size" import { BrowserSettings } from "../shared/BrowserSettings" +import { ADVISOR_SYSTEM_PROMPT } from "./prompts/advisor" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -93,6 +94,7 @@ export class Cline { checkpointTrackerErrorMessage?: string conversationHistoryDeletedRange?: [number, number] isInitialized = false + private advisorProblem?: string // streaming isStreaming = false @@ -105,6 +107,7 @@ export class Cline { private didRejectTool = false private didAlreadyUseTool = false private didCompleteReadingStream = false + private didAutomaticallyRetryFailedApiRequest = false constructor( provider: ClineProvider, @@ -1261,7 +1264,49 @@ export class Cline { this.conversationHistoryDeletedRange, ) - const stream = this.api.createMessage(systemPrompt, truncatedConversationHistory) + let stream = this.api.createMessage(systemPrompt, truncatedConversationHistory) + + // If we're consulting the advisor, override the request + const advisorModel = this.api.getAdvisorModel?.() + if (this.advisorProblem && advisorModel) { + // Generate markdown + const markdownContent = truncatedConversationHistory + .map((message) => { + const role = message.role === "user" ? "**User:**" : "**Coding Agent:**" + const content = Array.isArray(message.content) + ? message.content.map((block) => formatContentBlockToMarkdown(block)).join("\n") + : message.content + return `${role}\n\n${content}\n\n` + }) + .join("---\n\n") + + // Don't want to send the entire conv history, just the most recent context + // Get approximate char count from token limit + const advisorContextWindow = advisorModel.info.contextWindow || 128_000 + const tokensToKeep = Math.floor(advisorContextWindow / 2) + // Estimate ~3 chars per token as a rough approximation + const charsToKeep = tokensToKeep * 3 + // Get last n chars of markdown content + const isTruncated = markdownContent.length > charsToKeep + const recentContext = (isTruncated ? "... (truncated for brevity)\n\n" : "") + markdownContent.slice(-charsToKeep) + const advisorMessage: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: + "\n\nThe conversation history leading up to this point: " + + recentContext + + "\n\nThe problem the coding agent needs advice on: " + + this.advisorProblem, + }, + ], + }, + ] + stream = this.api.createMessage(ADVISOR_SYSTEM_PROMPT(), advisorMessage, "advisor") + } + const iterator = stream[Symbol.asyncIterator]() try { @@ -1269,13 +1314,23 @@ export class Cline { const firstChunk = await iterator.next() yield firstChunk.value } catch (error) { - // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. - const { response } = await this.ask("api_req_failed", error.message ?? JSON.stringify(serializeError(error), null, 2)) - if (response !== "yesButtonClicked") { - // this will never happen since if noButtonClicked, we will clear current task, aborting this instance - throw new Error("API request failed") + if (!this.didAutomaticallyRetryFailedApiRequest) { + console.log("first chunk failed, waiting 1 second before retrying") + await delay(1000) + this.didAutomaticallyRetryFailedApiRequest = true + } else { + // request failed after retrying automatically once, ask user if they want to retry again + // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. + const { response } = await this.ask( + "api_req_failed", + error.message ?? JSON.stringify(serializeError(error), null, 2), + ) + if (response !== "yesButtonClicked") { + // this will never happen since if noButtonClicked, we will clear current task, aborting this instance + throw new Error("API request failed") + } + await this.say("api_req_retried") } - await this.say("api_req_retried") // delegate generator output from the recursive call yield* this.attemptApiRequest(previousApiReqIndex) return @@ -1313,6 +1368,11 @@ export class Cline { const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too switch (block.type) { case "text": { + if (this.advisorProblem) { + await this.say("advisor_response", block.content, undefined, block.partial) + break + } + if (this.didRejectTool || this.didAlreadyUseTool) { break } @@ -2528,10 +2588,11 @@ export class Cline { } // now execute the tool - await this.say("consult_advisor_request_started") - const resourceResult = "Just try again bro." //await this.providerRef.deref()?.mcpHub?.readResource(server_name, uri) - await this.say("consult_advisor_response", resourceResult) - pushToolResult(formatResponse.toolResult(resourceResult)) + this.advisorProblem = problem + // await this.say("consult_advisor_request_started") + // const resourceResult = "Just try again bro." //await this.providerRef.deref()?.mcpHub?.readResource(server_name, uri) + // await this.say("consult_advisor_response", resourceResult) + pushToolResult(formatResponse.toolResult("Awaiting response from the Advisor model...")) await this.saveCheckpoint() break } @@ -2830,10 +2891,13 @@ export class Cline { // getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds // for the best UX we show a placeholder api_req_started message with a loading spinner as this happens + const advisorRequest = this.advisorProblem ? `(...conversation history)\n\n${this.advisorProblem}` : undefined await this.say( "api_req_started", JSON.stringify({ - request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...", + request: + advisorRequest || + userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...", }), ) @@ -2864,7 +2928,7 @@ export class Cline { // since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started") this.clineMessages[lastApiReqIndex].text = JSON.stringify({ - request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), + request: advisorRequest || userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), } satisfies ClineApiReqInfo) await this.saveClineMessages() await this.providerRef.deref()?.postStateToWebview() @@ -2944,8 +3008,11 @@ export class Cline { this.didAlreadyUseTool = false this.presentAssistantMessageLocked = false this.presentAssistantMessageHasPendingUpdates = false + this.didAutomaticallyRetryFailedApiRequest = false await this.diffViewProvider.reset() + const isCallingAdvisor = this.advisorProblem !== undefined + const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk) let assistantMessage = "" this.isStreaming = true @@ -3033,6 +3100,11 @@ export class Cline { await this.saveClineMessages() await this.providerRef.deref()?.postStateToWebview() + // If this last request was to the advisor model, then reset advisor problem to give control back to base model + if (isCallingAdvisor) { + this.advisorProblem = undefined + } + // now add to apiconversationhistory // need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response let didEndLoop = false @@ -3054,12 +3126,22 @@ export class Cline { // if the model did not tool use, then we need to tell it to either use a tool or attempt_completion const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use") + if (!didToolUse) { - this.userMessageContent.push({ - type: "text", - text: formatResponse.noToolsUsed(), - }) - this.consecutiveMistakeCount++ + if (isCallingAdvisor) { + // if the last request was a request to advisor then it wouldn't have used a tool + this.userMessageContent.push({ + type: "text", + text: "Please continue with the task, taking into account the advisor's response provided above.", + }) + } else { + // normal request where tool use is required + this.userMessageContent.push({ + type: "text", + text: formatResponse.noToolsUsed(), + }) + this.consecutiveMistakeCount++ + } } const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent) diff --git a/src/core/prompts/advisor.ts b/src/core/prompts/advisor.ts new file mode 100644 index 0000000000..267067d6c7 --- /dev/null +++ b/src/core/prompts/advisor.ts @@ -0,0 +1,52 @@ +export const ADVISOR_SYSTEM_PROMPT = + () => `You are a senior AI advisor with deep expertise in software development, system architecture, and technical problem-solving. Your role is to assist another AI agent by providing strategic guidance and solutions to coding challenges. + +==== + +INPUT FORMAT + +You will receive: +1. The autonomous agent's conversation history thus far +2. A specific problem or question the agent needs help with + +==== + +RESPONSE FORMAT + +Your responses should generally follow this structure: + +1. Problem Analysis +A summary of the context and key challenges, focusing on the most critical aspects that need to be addressed. + +2. Solution Approach +The recommended strategy or solution, broken down into clear, actionable steps. Include rationale for key decisions and potential trade-offs considered. Use specific technical guidance, including code snippets, architecture recommendations, or debugging strategies as needed. Focus on practical, implementable advice the agent can use to apply the solution. + +==== + +ADVISORY PRINCIPLES + +1. Focus on providing actionable, concrete guidance rather than theoretical discussions. Your advice should enable immediate progress. + +2. Consider both immediate solutions and long-term implications. Guide the agent toward maintainable, scalable solutions while solving the current problem. + +3. Adapt your guidance based on the context. Account for: +- Existing codebase and architecture +- Applied technologies and constraints +- Performance and scalability requirements +- Project conventions and standards + +4. When analyzing problems: +- Start with a systematic evaluation of the issue +- Consider common pitfalls and edge cases +- Look for patterns in error messages or behavior +- Think about interaction between system components + +5. For architectural guidance: +- Recommend established patterns when appropriate +- Consider system boundaries and integration points +- Address scalability and maintenance concerns +- Focus on practical, implementable solutions + +==== + +Remember: Your goal is to provide clear, actionable guidance that helps the agent make immediate progress while following good software development practices. Focus on practical solutions rather than theoretical discussions.` diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 1de338af9d..6f1ce9e123 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -97,10 +97,9 @@ export type ClineSay = | "mcp_server_response" | "use_mcp_server" | "consult_advisor" - | "consult_advisor_request_started" - | "consult_advisor_response" | "diff_error" | "deleted_api_reqs" + | "advisor_response" export interface ClineSayTool { tool: diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index acf02b5823..4ede2742c1 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -61,7 +61,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { // Careful not to use partials to mutate since spread operator only does shallow copy - const supportsAdvisor = apiConfiguration?.apiProvider === "openrouter" + const supportsAdvisor = apiConfiguration?.apiProvider === "openrouter" || apiConfiguration?.apiProvider === "anthropic" const actionMetadata = ACTION_METADATA.filter((action) => supportsAdvisor || action.id !== "consultAdvisor") const enabledActions = actionMetadata.filter((action) => autoApprovalSettings.actions[action.id]) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 7472b39725..6cb2df3bb8 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -124,7 +124,6 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING) const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started" - const isConsultAdvisorResponding = isLast && lastModifiedMessage?.say === "consult_advisor_request_started" const type = message.type === "ask" ? message.ask : message.say @@ -223,16 +222,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi case "consult_advisor": // const consultAdvisor = JSON.parse(message.text || "{}") as ClineConsultAdvisor return [ - isConsultAdvisorResponding ? ( - - ) : ( - - ), + , {message.type === "ask" ? ( <>Cline wants to consult the Advisor model about: @@ -884,6 +879,26 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi ) + case "advisor_response": + return ( +
+
+ Response +
+ +
+ ) case "user_feedback": return (
) - case "consult_advisor_response": - return ( - <> -
-
- Response -
- -
- - ) default: return ( <> diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 22c1b96638..70e879f497 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -198,6 +198,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie case "error": case "api_req_finished": case "text": + case "advisor_response": case "browser_action": case "browser_action_result": case "browser_action_launch": @@ -207,8 +208,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie case "command_output": case "mcp_server_request_started": case "mcp_server_response": - case "consult_advisor_request_started": - case "consult_advisor_response": case "completion_result": case "tool": break @@ -472,7 +471,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie } break case "mcp_server_request_started": - case "consult_advisor_request_started": return false } return true diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 52e7a170d8..213fcdf6d5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -818,7 +818,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad marginBottom: "10px", color: "var(--vscode-foreground)", }}> - This is the default driver model for Cline. It will read and edit files, run commands, and more, with + This model is the default driver for Cline. It will read and edit files, run commands, and more, with your permission at each step.

{selectedProvider === "anthropic" && ( @@ -846,8 +846,8 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad marginBottom: "10px", color: "var(--vscode-foreground)", }}> - The Cline model can call this smarter, more powerful model to ask for help on planning out a task, - fixing a hard bug, and other complex problems. + The Cline model can consult this smarter, more powerful model for help on planning out a task, fixing + a hard bug, and other complex problems.

{selectedProvider === "anthropic" && (