Implement advisor model calling

This commit is contained in:
Saoud Rizwan 2025-01-18 15:35:50 -08:00
parent 7e32e772cb
commit 21ae763f13
7 changed files with 184 additions and 60 deletions

View file

@ -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)

View file

@ -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.`

View file

@ -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:

View file

@ -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])

View file

@ -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 ? (
<ProgressIndicator />
) : (
<span
className="codicon codicon-server"
style={{
color: normalColor,
marginBottom: "-1.5px",
}}></span>
),
<span
className="codicon codicon-server"
style={{
color: normalColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>
{message.type === "ask" ? (
<>Cline wants to consult the Advisor model about:</>
@ -884,6 +879,26 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<Markdown markdown={message.text} />
</div>
)
case "advisor_response":
return (
<div style={{ paddingTop: 0 }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Response
</div>
<CodeAccordian
code={message.text}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
)
case "user_feedback":
return (
<div
@ -1089,28 +1104,6 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
</>
)
case "consult_advisor_response":
return (
<>
<div style={{ paddingTop: 0 }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Response
</div>
<CodeAccordian
code={message.text}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
</>
)
default:
return (
<>

View file

@ -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

View file

@ -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.
</p>
{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.
</p>
{selectedProvider === "anthropic" && (
<div className="dropdown-container" style={{ marginBottom: 15 }}>