Compare commits

...

12 Commits

Author SHA1 Message Date
abeatrix d9c6ba57f7 timeout ripgrep 2026-01-07 14:58:52 -08:00
abeatrix 90dae0f820 type 2026-01-07 14:58:32 -08:00
abeatrix 7e9e714cef generic 2025-12-31 14:26:36 -08:00
abeatrix 4e0d242453 split 2025-12-31 14:23:20 -08:00
abeatrix c3b31bc225 Includes Subagent cost in total cost 2025-12-31 12:39:19 -08:00
abeatrix 6775ce0085 use --no-hidden flag for search 2025-12-30 11:06:19 -08:00
abeatrix 603470b1c1 Merge branch 'main' into bee/agent-poc 2025-12-29 20:55:17 -08:00
abeatrix 7f1486a975 Merge branch 'main' into bee/agent-poc 2025-12-29 14:15:31 -08:00
abeatrix f0af58437b track serach queries and live updates 2025-12-19 16:43:48 -08:00
abeatrix 2ad585caea update to gemini 3 flash 2025-12-19 10:57:00 -08:00
abeatrix ceaa889acf wip 2025-12-06 05:41:11 -08:00
abeatrix 391c07c998 pod - agent 2025-12-06 05:40:48 -08:00
21 changed files with 1128 additions and 29 deletions
+384
View File
@@ -0,0 +1,384 @@
import { ApiHandler } from "@/core/api"
import { ClineHandler } from "@/core/api/providers/cline"
import type { ApiStream } from "@/core/api/transform/stream"
import { Logger } from "@/services/logging/Logger"
import { AgentActions, AgentContext, AgentIterationUpdate, ClineAgentConfig, SearchResult } from "@/shared/cline/subagent"
import type { ClineStorageMessage } from "@/shared/messages/content"
import type { ToolResponse } from "../task"
import type { TaskConfig } from "../task/tools/types/TaskConfig"
import { SubAgentToolDefinition, SubAgentToolResult } from "./tools"
import { extractTagContent } from "./utils"
/**
* Abstract base class for agentic loops using ClineHandler.
* Subclasses implement domain-specific logic for context management, tool execution, and result formatting.
*/
export abstract class ClineAgent {
protected readonly client: ApiHandler
protected currentIteration: number = 0
protected readonly maxIterations: number
protected readonly onIterationUpdate: (update: AgentIterationUpdate) => void | Promise<void>
protected cost = 0
protected readonly tools: Map<string, SubAgentToolDefinition> = new Map()
protected taskConfig?: TaskConfig
private static activeAgents = new Set<ClineAgent>()
constructor(private config: ClineAgentConfig) {
this.client = config.client ?? new ClineHandler({ openRouterModelId: config.modelId, ...config.apiParams })
this.maxIterations = config.maxIterations ?? 3
this.onIterationUpdate = config.onIterationUpdate
ClineAgent.activeAgents.add(this)
}
/**
* Collects and resets costs from all active agents
*/
static getAllAgentCosts(): number {
let totalCost = 0
for (const agent of ClineAgent.activeAgents) {
totalCost += agent.cost
agent.cost = 0
}
ClineAgent.activeAgents.clear()
if (totalCost > 0) {
Logger.debug(`Total cost across all agents: $${totalCost.toFixed(4)}`)
}
return totalCost
}
/**
* Registers tools for this agent
* @param toolDefinitions - Array of tool definitions to register
*/
protected registerTools(toolDefinitions: SubAgentToolDefinition[]): void {
for (const tool of toolDefinitions) {
this.tools.set(tool.title, tool)
}
}
/**
* Sets the task config for this agent (required for tool execution)
* @param taskConfig - The task configuration
*/
public setTaskConfig(taskConfig: TaskConfig): void {
this.taskConfig = taskConfig
}
/**
* Extracts tool calls from agent response based on registered tools.
* Parses the response for tool tags and extracts subtag values.
* @param response - The agent's response text
* @returns Map of tool tag to array of extracted input values
*/
protected extractToolCalls(response: string): Map<string, string[]> {
const toolCallsMap = new Map<string, string[]>()
for (const [toolTag, toolDef] of this.tools) {
const toolPattern = new RegExp(`<${toolTag}>(.*?)</${toolTag}>`, "gs")
const subTagPattern = new RegExp(`<${toolDef.tag}>(.*?)</${toolDef.tag}>`, "gs")
const inputs: string[] = []
for (const toolMatch of response.matchAll(toolPattern)) {
const toolContent = toolMatch[1]
for (const subTagMatch of toolContent.matchAll(subTagPattern)) {
const value = subTagMatch[1].trim()
if (value) {
inputs.push(value)
}
}
}
if (inputs.length > 0) {
toolCallsMap.set(toolTag, inputs)
}
}
return toolCallsMap
}
/**
* Executes a tool by its tag name
* @param toolTag - The tool tag (e.g., "TOOLFILE", "TOOLSEARCH")
* @param inputs - Array of input values for the tool
* @returns Promise resolving to the tool execution result
*/
protected async executeToolByTag(toolTag: string, inputs: string[]): Promise<SubAgentToolResult> {
const tool = this.tools.get(toolTag)
if (!tool) {
throw new Error(`Tool with tag "${toolTag}" not found in registered tools`)
}
if (!this.taskConfig) {
throw new Error(`TaskConfig not set. Call setTaskConfig() before executing tools.`)
}
return await tool.execute(inputs, this.taskConfig)
}
/**
* Builds the system prompt for the agent
* @param userInput - The user's input/query
* @param contextPrompt - The current context prompt
*/
abstract buildSystemPrompt(userInput: string, contextPrompt: string): string
/**
* Builds the context prompt for the current iteration
* @param context - The current agent context
* @param iteration - Current iteration number (0-indexed)
*/
abstract buildContextPrompt(context: AgentContext, iteration: number): string
/**
* Generic implementation of extractActions using registered tools and config tags.
* Extracts tool calls based on registered tools, context files, and ready-to-answer status.
* @param response - The full response text from the agent
*/
protected extractActions(response: string): AgentActions {
// Extract context files if contextTag is configured
const contextFiles = this.config.contextTag ? extractTagContent(response, this.config.contextTag) : []
// Check if ready to answer if answerTag is configured
const isReadyToAnswer = this.config.answerTag ? response.includes(`<${this.config.answerTag}>`) : false
// Extract tool calls based on registered tools
const toolCallsMap = this.extractToolCalls(response)
// Convert tool calls map to array format
const toolCalls: unknown[] = []
for (const [toolTag, inputs] of toolCallsMap) {
for (const input of inputs) {
toolCalls.push({ toolTag, input })
}
}
return {
toolCalls,
contextFiles,
isReadyToAnswer,
}
}
/**
* Generic tool execution that groups tool calls by tag and executes them in parallel.
* This is the recommended implementation for most agents.
* @param toolCallsMap - Map of tool tag to array of input values (use extractToolCalls to get this)
* @returns Promise resolving to map of tool tag to results
*/
protected async executeToolsByTag(toolCallsMap: Map<string, string[]>): Promise<Map<string, unknown>> {
const startTime = performance.now()
const entries = Array.from(toolCallsMap.entries())
// Execute all tools in parallel
const results = await Promise.all(entries.map(([toolTag, inputs]) => this.executeToolByTag(toolTag, inputs)))
// Build result map
const resultsByTag = new Map(entries.map(([toolTag], i) => [toolTag, results[i]]))
const totalCalls = entries.reduce((sum, [, inputs]) => sum + inputs.length, 0)
const duration = performance.now() - startTime
await this.onIterationUpdate({
iteration: this.currentIteration,
maxIterations: this.maxIterations,
message: `Executed ${totalCalls} tool calls in ${duration.toFixed(0)}ms`,
})
return resultsByTag
}
/**
* Executes tool calls in parallel
* @param toolCalls - Array of tool calls to execute
* @returns Promise resolving to array of tool results
*/
async executeTools(toolCalls: unknown[]): Promise<unknown[]> {
// Group tool calls by toolTag
const toolsByTag = new Map<string, string[]>()
for (const toolCall of toolCalls) {
if (typeof toolCall === "object" && toolCall !== null) {
const { toolTag, input } = toolCall as { toolTag: string; input: string }
const existing = toolsByTag.get(toolTag)
if (existing) {
existing.push(input)
} else {
toolsByTag.set(toolTag, [input])
}
}
}
// Execute all tools in parallel
const resultsByTag = await this.executeToolsByTag(toolsByTag)
// Reconstruct results in original order
const results: unknown[] = []
const indexByTag = new Map<string, number>()
for (const toolCall of toolCalls) {
if (typeof toolCall === "object" && toolCall !== null) {
const { toolTag } = toolCall as { toolTag: string; input: string }
const index = indexByTag.get(toolTag) ?? 0
const toolResults = resultsByTag.get(toolTag) as unknown[]
results.push(toolResults[index])
indexByTag.set(toolTag, index + 1)
}
}
return results
}
/**
* Reads context files in parallel
* @param filePaths - Array of file paths to read
* @returns Promise resolving to a map of file path to content
*/
abstract readContextFiles(filePaths: string[]): Promise<Map<string, string>>
/**
* Updates the context with new tool results
* @param context - Current context
* @param toolCalls - Tool calls that were executed
* @param toolResults - Results from tool execution
* @returns Whether new context was found
*/
abstract updateContextWithToolResults(context: AgentContext, toolCalls: unknown[], toolResults: unknown[]): boolean
/**
* Updates the context with file contents
* @param context - Current context
* @param fileContents - Map of file path to content
*/
protected updateContextWithFiles(context: AgentContext, fileContents: Map<string, string>): void {
for (const [filePath, content] of fileContents) {
context.fileContents.set(filePath, content)
}
}
/**
* Determines if the agent should continue iterating
* @param context - Current context
* @param iteration - Current iteration number
* @param foundNewContext - Whether new context was found in this iteration
* @param isReadyToAnswer - Whether the agent is ready to answer
*/
abstract shouldContinue(context: AgentContext, foundNewContext: boolean, isReadyToAnswer: boolean): boolean
/**
* Formats the final result from the context
* @param context - Final context state
*/
abstract formatResult(context: AgentContext): ToolResponse
/**
* Creates the initial context for the agent
*/
private createInitialContext(): AgentContext {
return {
filePaths: new Set<string>(),
searchResults: new Map<string, SearchResult>(),
fileContents: new Map<string, string>(),
}
}
/**
* Processes a streaming response and accumulates text and cost
*/
private async processStream(stream: ApiStream): Promise<string> {
const parts: string[] = []
for await (const msg of stream) {
if (msg.type === "text") {
parts.push(msg.text)
}
if (msg.type === "usage" && msg.totalCost) {
this.cost += msg.totalCost
await this.onIterationUpdate({
iteration: this.currentIteration,
maxIterations: this.maxIterations,
cost: msg.totalCost,
})
}
}
return parts.join("")
}
/**
* Executes the agentic loop
* @param userInput - The user's input/query
* @returns Promise resolving to the final result
*/
public async execute(userInput: string): Promise<ToolResponse> {
const startTime = performance.now()
const context = this.createInitialContext()
for (let iteration = 0; iteration < this.maxIterations; iteration++) {
this.currentIteration = iteration + 1
// Build context prompt and system prompt
const contextPrompt = this.buildContextPrompt(context, iteration)
const systemPrompt = this.buildSystemPrompt(userInput, contextPrompt)
// Create messages
const messages: ClineStorageMessage[] = this.config.messages
? [...this.config.messages, { role: "user", content: userInput }]
: [{ role: "user", content: userInput }]
// Stream the LLM response
const stream = this.client.createMessage(systemPrompt, messages)
const fullResponse = await this.processStream(stream)
Logger.log(`Iteration ${this.currentIteration} response: ${fullResponse.substring(0, 200)}`)
// Extract actions from response
const actions = this.extractActions(fullResponse)
// Send iteration update
await this.onIterationUpdate({
iteration,
maxIterations: this.maxIterations,
actions,
context,
})
// If ready to answer and has context files, read them first
if (actions.isReadyToAnswer && actions.contextFiles.length > 0) {
Logger.log(
`Reading ${actions.contextFiles.length} context files before answering: ${actions.contextFiles.join(", ")}`,
)
const fileContents = await this.readContextFiles(actions.contextFiles)
this.updateContextWithFiles(context, fileContents)
Logger.log("Agent determined it has enough context to answer.")
break
}
// If ready to answer without context files, break immediately
if (actions.isReadyToAnswer) {
Logger.log("Agent determined it has enough context to answer.")
break
}
// If no tool calls, end the loop
if (actions.toolCalls.length === 0) {
Logger.log("No tool calls generated, ending loop.")
break
}
// Execute tools in parallel
Logger.log(`Executing ${actions.toolCalls.length} tool calls`)
const toolResults = await this.executeTools(actions.toolCalls)
// Update context with tool results
const foundNewContext = this.updateContextWithToolResults(context, actions.toolCalls, toolResults)
// Check if we should continue
if (!this.shouldContinue(context, foundNewContext, false)) {
Logger.log("Agent determined it should stop iterating.")
break
}
}
const duration = performance.now() - startTime
Logger.debug("Agent completed in " + duration)
// Format and return final result
return this.formatResult(context)
}
}
+223
View File
@@ -0,0 +1,223 @@
import { AgentContext, AgentIterationUpdate, FileReadResult, SearchResult } from "@/shared/cline/subagent"
import { ToolResponse } from "../task"
import type { TaskConfig } from "../task/tools/types/TaskConfig"
import { ClineAgent } from "./ClineAgent"
import { SEARCH_AGENT_TOOLS } from "./tools"
import { buildToolsPlaceholder } from "./utils"
export const ACTIONS_TAGS = {
ANSWER: `next_step`,
CONTEXT: `context_list`,
}
const SEARCH_MODELS = {
grok: "x-ai/grok-code-fast-1",
gemini: "google/gemini-3-flash-preview",
}
/**
* SearchAgent extends ClineAgent to provide natural language search functionality
* across codebases using an agentic loop.
*/
export class SearchAgent extends ClineAgent {
constructor(
taskConfig: TaskConfig,
maxIterations: number = 3,
onIterationUpdate: (update: AgentIterationUpdate) => void | Promise<void>,
systemPrompt?: string,
modelId: string = SEARCH_MODELS.gemini,
) {
super({
modelId,
maxIterations,
onIterationUpdate,
systemPrompt,
contextTag: ACTIONS_TAGS.CONTEXT,
answerTag: ACTIONS_TAGS.ANSWER,
})
this.setTaskConfig(taskConfig)
this.registerTools(SEARCH_AGENT_TOOLS)
}
buildSystemPrompt(userInput: string, contextPrompt: string): string {
return buildSearchAgentSystemPrompt(userInput, contextPrompt, ACTIONS_TAGS)
}
buildContextPrompt(context: AgentContext, iteration: number): string {
if (iteration === 0 || (context.searchResults.size === 0 && context.fileContents.size === 0)) {
return "No context retrieved yet."
}
const MAX_RESULTS_TO_SHOW = 5
const contextParts: string[] = []
const successfulQueries: string[] = []
const unsuccessfulQueries: string[] = []
// Process search results
let shownResults = 0
for (const [query, result] of context.searchResults) {
if (result.success && result.resultCount > 0) {
successfulQueries.push(query)
if (shownResults < MAX_RESULTS_TO_SHOW) {
contextParts.push(`### Search: "${query}"\n${result.workspaceResults}`)
shownResults++
} else {
contextParts.push(
`### Search: "${query}"\nFound ${result.resultCount} result${result.resultCount > 1 ? "s" : ""}`,
)
}
} else {
unsuccessfulQueries.push(query)
}
}
// Add file contents
for (const [filePath, content] of context.fileContents) {
contextParts.push(`### File: ${filePath}\n\`\`\`\n${content}\n\`\`\``)
}
// Build header
const totalSearches = context.searchResults.size
const totalFiles = context.filePaths.size
const totalFileContents = context.fileContents.size
const parts = [
`Retrieved context from ${totalSearches} search${totalSearches > 1 ? "es" : ""} (${totalFiles} unique file${totalFiles > 1 ? "s" : ""}) and ${totalFileContents} file content${totalFileContents > 1 ? "s" : ""}:\n`,
]
// Add search history
if (successfulQueries.length > 0 || unsuccessfulQueries.length > 0) {
parts.push("\n**Previously searched queries (DO NOT search these again):**")
successfulQueries.forEach((q) => parts.push(`- "${q}" ✓ (found results)`))
unsuccessfulQueries.forEach((q) => parts.push(`- "${q}" ✗ (no results)`))
parts.push("")
}
return parts.join("\n") + contextParts.join("\n\n")
}
async readContextFiles(filePaths: string[]): Promise<Map<string, string>> {
const fileResults = (await this.executeToolByTag("TOOLFILE", filePaths)) as FileReadResult[]
const fileContents = new Map<string, string>()
for (const fileResult of fileResults) {
if (fileResult.success) {
fileContents.set(fileResult.path, fileResult.content)
}
}
return fileContents
}
public updateContextWithToolResults(context: AgentContext, toolCalls: unknown[], toolResults: unknown[]): boolean {
const initialFileCount = context.filePaths.size
const initialContentCount = context.fileContents.size
for (let i = 0; i < toolCalls.length; i++) {
const toolCall = toolCalls[i]
const toolResult = toolResults[i]
if (typeof toolCall === "object" && toolCall !== null) {
const { toolTag, input } = toolCall as { toolTag: string; input: string }
if (toolTag === "TOOLSEARCH" && toolResult) {
const result = toolResult as SearchResult
if (result.success && result.resultCount > 0) {
this.extractFilePathsFromResult(result).forEach((fp) => context.filePaths.add(fp))
context.searchResults.set(input, result)
}
} else if (toolTag === "TOOLFILE" && toolResult) {
const fileResult = toolResult as FileReadResult
if (fileResult.success && !context.fileContents.has(fileResult.path)) {
context.fileContents.set(fileResult.path, fileResult.content)
}
}
}
}
return context.filePaths.size > initialFileCount || context.fileContents.size > initialContentCount
}
public shouldContinue(_context: AgentContext, foundNewContext: boolean, isReadyToAnswer: boolean): boolean {
return !isReadyToAnswer && foundNewContext && this.currentIteration < this.maxIterations - 1
}
public formatResult(context: AgentContext, pathOnly = true): ToolResponse {
// Return file contents if available
if (context.fileContents.size > 0) {
const paths = Array.from(context.fileContents.keys())
if (pathOnly) {
const count = paths.length
const label = count === 1 ? "1 file" : `${count} files`
return `Search Agent returned ${label}:\n${paths.map((p) => `- ${p}`).join("\n")}`
}
return [...context.fileContents].map(([filePath, content]) => ({ type: "text", text: `${filePath}\n${content}` }))
}
// Return search results
if (context.filePaths.size === 0) {
return "No results found after searching."
}
const paths = Array.from(context.filePaths)
const count = paths.length
const label = count === 1 ? "1 file" : `${count} files`
return `Found ${label} across multiple searches:\n${paths.map((p) => `- ${p}`).join("\n")}`
}
private extractFilePathsFromResult(result: SearchResult): string[] {
return result.workspaceResults
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("│") && !line.startsWith("Found ") && !line.startsWith("Showing "))
}
}
/**
* Builds the complete system prompt for SearchAgent.
* Replaces all template placeholders with actual values.
*/
function buildSearchAgentSystemPrompt(userInput: string, contextPrompt: string, actionsTags: typeof ACTIONS_TAGS): string {
const toolsPlaceholder = buildToolsPlaceholder(SEARCH_AGENT_TOOLS)
return `You are a context review agent. Evaluate the shared context and determine if you can answer the user's request.
## CURRENT CONTEXT
${contextPrompt}
## TOOLS
Available tools to fetch additional context:
- ${toolsPlaceholder}
## RESPONSE FORMAT
Your response must contain ONLY tags (no explanations, no markdown blocks). Choose one:
**If you have enough context:**
- List relevant files/contexts with <${actionsTags.CONTEXT}> tags (only from CURRENT CONTEXT above)
- End with <${actionsTags.ANSWER}>
- Example: <${actionsTags.CONTEXT}>file1.ts</${actionsTags.CONTEXT}><${actionsTags.CONTEXT}>file2.ts</${actionsTags.CONTEXT}><${actionsTags.ANSWER}>
**If you need NO context:**
- Respond with: <${actionsTags.ANSWER}>
**If you need more context:**
- Use <TOOL*> tags to request it
- Example: <TOOLFILE><name>path/to/file.ts</name></TOOLFILE><TOOLSEARCH><query>class Controller</query></TOOLSEARCH>
## RULES
- Only include files/contexts from CURRENT CONTEXT in <${actionsTags.CONTEXT}> tags
- Never include empty <${actionsTags.CONTEXT}></${actionsTags.CONTEXT}> tags
- Check CURRENT CONTEXT before requesting - avoid duplicate searches
- Use multiple <TOOL*> tags in parallel if needed
- Response must be ONLY tags, nothing else
## INVALID OUTPUT (DO NOT DO THIS)
- Empty context: <${actionsTags.CONTEXT}></${actionsTags.CONTEXT}>
- Explanations: <${actionsTags.ANSWER}> your explanation here
- Non-shared context: <${actionsTags.CONTEXT}>not-in-context.ts</${actionsTags.CONTEXT}>
<user_input>
${userInput}
</user_input>
Remember: Your response will be parsed by a bot. Only include the expected tags.`
}
+126
View File
@@ -0,0 +1,126 @@
import { extractFileContent } from "@/integrations/misc/extract-file-content"
import { Logger } from "@/services/logging/Logger"
import { regexSearchFiles } from "@/services/ripgrep"
import { FileReadResult, SearchResult } from "@/shared/cline/subagent"
import { TaskConfig } from "../task/tools/types/TaskConfig"
import { resolveWorkspacePath } from "../workspace"
export type SubAgentToolResult = FileReadResult[] | SearchResult[]
export interface SubAgentToolDefinition {
title: string
tag: string
instruction: string
placeholder: string
examples?: string[]
execute: (inputs: string[], taskConfig: TaskConfig) => Promise<SubAgentToolResult>
}
const TOOLFILE: SubAgentToolDefinition = {
title: "TOOLFILE",
tag: "name",
instruction:
"To retrieve full content of a codebase file using absolute path filename-DO NOT retrieve files that may contain secrets",
placeholder: "ABSOLUTE_PATH",
examples: [`See the content of different files: \`<TOOLFILE><name>path/foo.ts</name><name>path/bar.ts</name></TOOLFILE>\``],
execute: async (filePaths: string[], taskConfig: TaskConfig): Promise<SubAgentToolResult> => {
const fileReadPromises = filePaths.map(async (filePath) => {
try {
// Resolve the file path relative to the workspace
const pathResult = resolveWorkspacePath(taskConfig, filePath, "SubAgent.executeParallelFileReads")
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
// Read the file content
const supportsImages = taskConfig.api.getModel().info.supportsImages ?? false
const fileContent = await extractFileContent(absolutePath, supportsImages)
Logger.info(`Read file content for "${filePath}" successfully.`)
return {
path: filePath,
content: fileContent.text,
success: true,
}
} catch (error) {
Logger.error(`File read failed for "${filePath}": ${error instanceof Error ? error.message : String(error)}`)
return {
path: filePath,
content: `Error reading file: ${error instanceof Error ? error.message : String(error)}`,
success: false,
}
}
})
return await Promise.all(fileReadPromises)
},
}
const TOOLSEARCH: SubAgentToolDefinition = {
title: "TOOLSEARCH",
tag: "query",
instruction:
"Perform regex pattern searches across the codebase. Supports multiple parallel searches by including multiple query tags. All searches will execute simultaneously for faster results",
placeholder: "SEARCH_QUERY",
examples: [
`Single search: \`<TOOLSEARCH><query>symbol name</query></TOOLSEARCH>\``,
`Single search with REGEX query: \`<TOOLSEARCH><query>class \w+Handler.*ApiHandler|export.*ApiHandler|ApiProvider|ModelProvider</query></TOOLSEARCH>\``,
`Multiple parallel searches: \`<TOOLSEARCH><query>getController</query></TOOLSEARCH><TOOLSEARCH><query>AuthService</query></TOOLSEARCH>\``,
`Search for a class definition: \`<TOOLSEARCH><query>class UserController</query></TOOLSEARCH>\``,
],
execute: async (queries: string[], taskConfig: TaskConfig): Promise<SubAgentToolResult> => {
const executeSearch = async (absolutePath: string, query: string) => {
try {
const workspaceResults = await regexSearchFiles(
taskConfig.cwd,
absolutePath,
query,
undefined,
taskConfig.services.clineIgnoreController,
false, // exclude hidden files
)
const firstLine = workspaceResults.split("\n")[0]
// Match either "Found X result(s)" or "Showing first X of X+ results"
const resultMatch = firstLine.match(/Found (\d+) result|Showing first (\d+) of/)
const resultCount = resultMatch ? parseInt(resultMatch[1] || resultMatch[2], 10) : 0
Logger.info(`Search for "${query}" found ${resultCount} results in ${absolutePath}`)
return {
query,
workspaceResults,
resultCount,
success: true,
}
} catch (error) {
Logger.error(`Search failed in ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`)
return {
query,
workspaceResults: "",
resultCount: 0,
success: false,
}
}
}
const searchPromises = queries.map(async (query) => {
try {
const searchPath = taskConfig.cwd
return await executeSearch(searchPath, query)
} catch (error) {
Logger.error(`Search failed for query "${query}": ${error instanceof Error ? error.message : String(error)}`)
return {
query,
workspaceResults: "",
resultCount: 0,
success: false,
}
}
})
return await Promise.all(searchPromises)
},
}
/**
* Tools configuration for SearchAgent.
*/
export const SEARCH_AGENT_TOOLS: SubAgentToolDefinition[] = [TOOLFILE, TOOLSEARCH]
+26
View File
@@ -0,0 +1,26 @@
import { SubAgentToolDefinition } from "./tools"
/**
* Builds the tools placeholder string for the system prompt.
* Formats all tool definitions into a readable instruction format.
*/
export function buildToolsPlaceholder(tools: SubAgentToolDefinition[]): string {
const toolsPrompts: string[] = []
for (const tool of tools) {
const prompt = `\`<${tool.title}><${tool.tag}>${tool.placeholder}</${tool.tag}></${tool.title}>\`: ${tool.instruction}.`
if (tool.examples && tool.examples.length > 0) {
toolsPrompts.push(`${prompt}\n\t- ${tool.examples.join("\n\t- ")}`)
} else {
toolsPrompts.push(prompt)
}
}
return toolsPrompts.join("\n")
}
export function extractTagContent(response: string, tag: string): string[] {
const tagLength = tag.length
return response.match(new RegExp(`<${tag}>(.*?)</${tag}>`, "g"))?.map((m) => m.slice(tagLength + 2, -(tagLength + 3))) || []
}
@@ -77,12 +77,12 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
id,
name: "attempt_completion",
description:
"Once you've completed the user's task, use this tool to present the final result to the user, including a brief and very short (1-2 paragraph) summary of the task and what was done to resolve it. Provide the basics, hitting the highlights, but do delve into the specifics. You should only call this tool when you have completed all tasks in the task_progress list, and completed all changes that are necessary to satisfy the user's request. You should not provide the contents of the task_progress list in the result parameter, it must be included in the task_progress parameter.",
"Once you've completed the user's task, or have all the information you need to to answer user's question, ALWAYS use this tool to present the final result to the user, including a brief and very short (1-2 paragraph) summary of the task and what was done to resolve it. Provide the basics, hitting the highlights, but do delve into the specifics. You should only call this tool when you have completed all tasks in the task_progress list, and completed all changes that are necessary to satisfy the user's request. You should not provide the contents of the task_progress list in the result parameter, it must be included in the task_progress parameter.",
parameters: [
{
name: "result",
required: true,
instruction: "A clear, brief and very short (1-2 paragraph) summary of the final result of the task.",
instruction: "Summary of the final result of the task, or the final answer to the user's question.",
},
{
name: "command",
@@ -15,6 +15,7 @@ export * from "./plan_mode_respond"
export * from "./read_file"
export * from "./replace_in_file"
export * from "./search_files"
export * from "./search_subagent"
export * from "./use_mcp_tool"
export * from "./web_fetch"
export * from "./web_search"
@@ -17,6 +17,7 @@ import { plan_mode_respond_variants } from "./plan_mode_respond"
import { read_file_variants } from "./read_file"
import { replace_in_file_variants } from "./replace_in_file"
import { search_files_variants } from "./search_files"
import { search_agents_variants } from "./search_subagent"
import { use_mcp_tool_variants } from "./use_mcp_tool"
import { web_fetch_variants } from "./web_fetch"
import { web_search_variants } from "./web_search"
@@ -45,6 +46,7 @@ export function registerClineToolSets(): void {
...plan_mode_respond_variants,
...read_file_variants,
...replace_in_file_variants,
...search_agents_variants,
...search_files_variants,
...use_mcp_tool_variants,
...web_fetch_variants,
@@ -0,0 +1,24 @@
import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import type { ClineToolSpec } from "../spec"
import { TASK_PROGRESS_PARAMETER } from "../types"
const id = ClineDefaultTool.SEARCH_AGENT
const NATIVE_NEXT_GEN: ClineToolSpec = {
variant: ModelFamily.NATIVE_NEXT_GEN,
id,
name: id,
description:
"Search context using natural language input to find relevant context across different sources (codebase, files, etc.). The provided input should be a full descriptive phrase or question that'd allow the search agent to understand what you are looking for to formulate an effective search strategy that returns relevant context as results.",
parameters: [
{
name: "input",
required: true,
instruction: `A detailed, complete natural language description or question about what you are trying to find, like 'What authentication providers are used in the codebase?' or 'The file that defined DBController symbol.' for example. IMPORTANT: Combining individual search terms or keywords like 'authentication auth providers login oauth' or 'database, config, connections' are not valid inputs and must be avoided.`,
},
TASK_PROGRESS_PARAMETER,
],
}
export const search_agents_variants = [NATIVE_NEXT_GEN]
@@ -48,7 +48,7 @@ export const config = createVariant(ModelFamily.NATIVE_NEXT_GEN)
ClineDefaultTool.FILE_READ,
ClineDefaultTool.FILE_NEW,
ClineDefaultTool.FILE_EDIT,
ClineDefaultTool.SEARCH,
ClineDefaultTool.SEARCH_AGENT,
ClineDefaultTool.LIST_FILES,
ClineDefaultTool.LIST_CODE_DEF,
ClineDefaultTool.BROWSER,
@@ -74,15 +74,16 @@ In each user message, the environment_details will specify the current mode. The
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.`
const OBJECTIVE = (context: SystemPromptContext) => `OBJECTIVE
const OBJECTIVE = (_context: SystemPromptContext) => `OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params)${context.yoloModeToggled !== true ? " and instead, ask the user to provide the missing parameters using the ask_followup_question tool" : ""}. DO NOT ask for more information on optional parameters if it is not provided.
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. If the task is not actionable, you may use the attempt_completion tool to explain to the user why the task cannot be completed, or provide a simple answer if that is what the user is looking for.`
1. Always gather all the necessary context before starting to work on a task. For example, if you are generating a unit test or new code, make sure you understand the requirement, the naming conventions, frameworks and libraries used and aligned in the current codebase, and the environment and commands used to run and test the code etc. Always validate the new unit test at the end including running the code if possible for live feedback.
2. Review each question carefully and answer it with detailed, accurate information.
3. If you need more information, use one of the available tools or ask for clarification instead of making assumptions or lies.
4. If the task is not actionable, you may use the attempt_completion tool to explain to the user why the task cannot be completed, or provide a simple answer if that is what the user is looking for.
IMPORTANT: Always uses the attempt_completion tool when you've completed all tasks, including giving your answer to the user question.`
const FEEDBACK = (_context: SystemPromptContext) => `FEEDBACK
@@ -75,4 +75,5 @@ export const rules_template = (context: SystemPromptContext) => `RULES
- When using the replace_in_file tool, if you use multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. For example if you need to make changes to both line 10 and line 50, first include the SEARCH/REPLACE block for line 10, followed by the SEARCH/REPLACE block for line 50.
- When using the replace_in_file tool, Do NOT add extra characters to the markers (e.g., ------- SEARCH> is INVALID). Do NOT forget to use the closing +++++++ REPLACE marker. Do NOT modify the marker format in any way. Malformed XML will cause complete tool failure and break the entire editing process.
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.{{BROWSER_WAIT_RULES}}
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.`
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
- When user asked a question, always provide the final answer using the attempt_completion tool rather than answering directly in your response.`
+4 -1
View File
@@ -37,6 +37,7 @@ import { PlanModeRespondHandler } from "./tools/handlers/PlanModeRespondHandler"
import { ReadFileToolHandler } from "./tools/handlers/ReadFileToolHandler"
import { ReportBugHandler } from "./tools/handlers/ReportBugHandler"
import { SearchFilesToolHandler } from "./tools/handlers/SearchFilesToolHandler"
import { SearchSubAgentHandler } from "./tools/handlers/SearchSubAgentHandler"
import { SummarizeTaskHandler } from "./tools/handlers/SummarizeTaskHandler"
import { UseMcpToolHandler } from "./tools/handlers/UseMcpToolHandler"
import { WebFetchToolHandler } from "./tools/handlers/WebFetchToolHandler"
@@ -216,8 +217,10 @@ export class ToolExecutor {
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.FILE_EDIT, writeHandler))
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.NEW_RULE, writeHandler))
this.coordinator.register(new ListCodeDefinitionNamesToolHandler(validator))
this.coordinator.register(new SearchSubAgentHandler())
this.coordinator.register(new SearchFilesToolHandler(validator))
this.coordinator.register(new ListCodeDefinitionNamesToolHandler(validator))
this.coordinator.register(new ExecuteCommandToolHandler(validator))
this.coordinator.register(new UseMcpToolHandler())
this.coordinator.register(new AccessMcpResourceHandler())
+16
View File
@@ -93,6 +93,7 @@ import {
} from "@/shared/messages"
import { ShowMessageType } from "@/shared/proto/index.host"
import { isClineCliInstalled, isCliSubagentContext } from "@/utils/cli-detector"
import { ClineAgent } from "../agents/ClineAgent"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { Controller } from "../controller"
@@ -2430,6 +2431,10 @@ export class Task {
console.log("updating partial message", lastMessage)
// await this.saveClineMessagesAndUpdateHistory()
}
const subAgentCosts = ClineAgent.getAllAgentCosts()
if (subAgentCosts) {
taskMetrics.totalCost = (taskMetrics.totalCost ?? 0) + subAgentCosts
}
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
@@ -2528,6 +2533,7 @@ export class Task {
taskMetrics.cacheWriteTokens += chunk.cacheWriteTokens ?? 0
taskMetrics.cacheReadTokens += chunk.cacheReadTokens ?? 0
taskMetrics.totalCost = chunk.totalCost ?? taskMetrics.totalCost
break
case "reasoning": {
// Process the reasoning delta through the handler
@@ -2602,6 +2608,11 @@ export class Task {
}
}
const subAgentCosts = ClineAgent.getAllAgentCosts()
if (subAgentCosts) {
taskMetrics.totalCost = (taskMetrics.totalCost ?? 0) + subAgentCosts
}
// present content to user - we don't want the stream to break if present fails, so we catch errors here
await this.presentAssistantMessage().catch((error) =>
Logger.debug("[Task] Failed to present message: " + error),
@@ -2702,6 +2713,11 @@ export class Task {
})
}
const subAgentCosts = ClineAgent.getAllAgentCosts()
if (subAgentCosts) {
taskMetrics.totalCost = (taskMetrics.totalCost ?? 0) + subAgentCosts
}
// Update the api_req_started message with final usage and cost details
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
@@ -7,6 +7,7 @@ import { parseWorkspaceInlinePath } from "@/core/workspace/utils/parseWorkspaceI
import { WorkspacePathAdapter } from "@/core/workspace/WorkspacePathAdapter"
import { resolveWorkspacePath } from "@/core/workspace/WorkspaceResolver"
import { telemetryService } from "@/services/telemetry"
import { SearchResult } from "@/shared/cline/subagent"
import { ClineSayTool } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
@@ -77,7 +78,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
absolutePath: string,
workspaceName: string | undefined,
workspaceRoot: string | undefined,
regex: string,
query: string,
filePattern: string | undefined,
) {
try {
@@ -87,7 +88,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const workspaceResults = await regexSearchFiles(
basePathForRelative,
absolutePath,
regex,
query,
filePattern,
config.services.clineIgnoreController,
)
@@ -98,6 +99,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const resultCount = resultMatch ? parseInt(resultMatch[1], 10) : 0
return {
query,
workspaceName,
workspaceResults,
resultCount,
@@ -107,6 +109,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
// If search fails in one workspace, return error info
console.error(`Search failed in ${absolutePath}:`, error)
return {
query,
workspaceName,
workspaceResults: "",
resultCount: 0,
@@ -120,12 +123,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
*/
private formatSearchResults(
config: TaskConfig,
searchResults: Array<{
workspaceName?: string
workspaceResults: string
resultCount: number
success: boolean
}>,
searchResults: Array<SearchResult>,
searchPaths: Array<{ absolutePath: string; workspaceName?: string }>,
): string {
const allResults: string[] = []
@@ -0,0 +1,112 @@
import type { ToolUse } from "@core/assistant-message"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { SearchAgent } from "@/core/agents/SearchAgent"
import { ClineSayTool } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class SearchSubAgentHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.SEARCH_AGENT
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
private async buildToolMessage(config: TaskConfig, block: ToolUse, content: string, parsedPath: string): Promise<string> {
const sharedProps: ClineSayTool = {
tool: block.params.input ? "searchAgent" : "searchFiles",
path: getReadablePath(config.cwd, block.params.path),
content,
regex: block.params.regex,
filePattern: block.params.input || block.params.file_pattern,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(parsedPath),
}
return JSON.stringify(sharedProps)
}
private async buildPartialToolMessage(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<string> {
const config = uiHelpers.getConfig()
const sharedProps: ClineSayTool = {
tool: block.params.input ? "searchAgent" : "searchFiles",
path: getReadablePath(config.cwd, uiHelpers.removeClosingTag(block, "path", block.params.path)),
content: "",
regex: uiHelpers.removeClosingTag(block, "regex", block.params.regex),
filePattern: block.params.input || uiHelpers.removeClosingTag(block, "file_pattern", block.params.file_pattern),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
}
return JSON.stringify(sharedProps)
}
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const partialMessage = await this.buildPartialToolMessage(block, uiHelpers)
const isAutoApprove = await uiHelpers.shouldAutoApproveToolWithPath(block.name, block.params.path)
if (isAutoApprove) {
await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "tool")
await uiHelpers.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "tool")
await uiHelpers.ask("tool", partialMessage, block.partial).catch(() => {})
}
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
const searchInput: string | undefined = block.params.input
if (!searchInput) {
throw new Error("Search input is required for SearchSubAgentHandler.")
}
return await this.performNaturalLanguageSearch(config, block, searchInput)
}
private async performNaturalLanguageSearch(config: TaskConfig, block: ToolUse, searchInput: string): Promise<ToolResponse> {
try {
const agent = new SearchAgent(config, 3, async (update) => {
// Build a partial message showing the progress
const progress = `[${update.iteration + 1}/${update.maxIterations}]`
let statusText = progress
if (update.message) {
statusText += ` - ${update.message}`
}
if (update.actions) {
const toolCallCount = update.actions.toolCalls.length
const contextFileCount = update.actions.contextFiles.length
if (update.actions.isReadyToAnswer) {
statusText += ` - Ready to answer`
if (contextFileCount > 0) {
statusText += ` with ${contextFileCount} file${contextFileCount > 1 ? "s" : ""}`
}
} else if (toolCallCount > 0) {
statusText += ` - Executing ${toolCallCount} tool call${toolCallCount > 1 ? "s" : ""}`
}
}
const partialMessage = await this.buildToolMessage(config, block, statusText, block.params.path || "")
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", partialMessage, undefined, undefined, true)
})
const searchResults = await agent.execute(searchInput)
const formattedResults = Array.isArray(searchResults)
? searchResults?.map((r) => (r.type === "text" ? r.text : ""))?.join("\n\n")
: searchResults
const completeMessage = await this.buildToolMessage(config, block, formattedResults, block.params.path || "")
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
return searchResults
} catch (error) {
console.error("Natural language search error:", error)
return `Natural language search error: ${error instanceof Error ? error.message : String(error)}`
}
}
}
+85 -6
View File
@@ -3,6 +3,7 @@ import * as childProcess from "child_process"
import * as path from "path"
import * as readline from "readline"
import { getBinaryLocation } from "@/utils/fs"
import "@/utils/path" // necessary to have access to String.prototype.toPosix
/*
This file provides functionality to perform regex searches on files using ripgrep.
@@ -55,8 +56,10 @@ interface SearchResult {
}
const MAX_RESULTS = 300
// Default timeout for ripgrep searches (30 seconds)
const DEFAULT_RIPGREP_TIMEOUT_MS = 30_000
async function execRipgrep(args: string[]): Promise<string> {
async function execRipgrep(args: string[], timeoutMs: number = DEFAULT_RIPGREP_TIMEOUT_MS): Promise<string> {
const binPath: string = await getBinaryLocation("rg")
return new Promise((resolve, reject) => {
@@ -69,31 +72,101 @@ async function execRipgrep(args: string[]): Promise<string> {
let output = ""
let lineCount = 0
let isResolved = false
let sigkillTimeout: NodeJS.Timeout | null = null
const maxLines = MAX_RESULTS * 5 // limiting ripgrep output with max lines since there's no other way to limit results. it's okay that we're outputting as json, since we're parsing it line by line and ignore anything that's not part of a match. This assumes each result is at most 5 lines.
// Cleanup function to ensure process and listeners are properly cleaned up
const cleanup = () => {
// Clear any pending SIGKILL timeout
if (sigkillTimeout) {
clearTimeout(sigkillTimeout)
sigkillTimeout = null
}
if (rgProcess && !rgProcess.killed) {
rgProcess.kill("SIGTERM")
// Force kill if still running after a short delay
sigkillTimeout = setTimeout(() => {
if (rgProcess && !rgProcess.killed) {
rgProcess.kill("SIGKILL")
}
sigkillTimeout = null
}, 1000)
}
rl.removeAllListeners()
rgProcess.stdout?.removeAllListeners()
rgProcess.stderr?.removeAllListeners()
rgProcess.removeAllListeners()
}
// Set up timeout
const timeoutHandle = setTimeout(() => {
if (!isResolved) {
isResolved = true
cleanup()
reject(new Error(`ripgrep process timed out after ${timeoutMs}ms`))
}
}, timeoutMs)
// Helper to resolve/reject once
const finish = (result: "resolve" | "reject", value?: string | Error) => {
if (isResolved) {
return
}
isResolved = true
clearTimeout(timeoutHandle)
cleanup()
if (result === "resolve") {
resolve(value as string)
} else {
reject(value as Error)
}
}
rl.on("line", (line) => {
if (isResolved) {
return
}
if (lineCount < maxLines) {
output += line + "\n"
lineCount++
} else {
rl.close()
rgProcess.kill()
rgProcess.kill("SIGTERM")
}
})
let errorOutput = ""
rgProcess.stderr.on("data", (data) => {
if (isResolved) {
return
}
errorOutput += data.toString()
})
rl.on("close", () => {
if (isResolved) {
return
}
if (errorOutput) {
reject(new Error(`ripgrep process error: ${errorOutput}`))
finish("reject", new Error(`ripgrep process error: ${errorOutput}`))
} else {
resolve(output)
finish("resolve", output)
}
})
rgProcess.on("error", (error) => {
reject(new Error(`ripgrep process error: ${error.message}`))
if (isResolved) {
return
}
finish("reject", new Error(`ripgrep process error: ${error.message}`))
})
rgProcess.on("exit", (code, signal) => {
// If process exits before we've resolved, handle it
if (!isResolved && code !== null && code !== 0) {
finish("reject", new Error(`ripgrep process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`))
}
})
})
}
@@ -104,12 +177,18 @@ export async function regexSearchFiles(
regex: string,
filePattern?: string,
clineIgnoreController?: ClineIgnoreController,
includesHiddenFiles = true, // Includes hidden files and ignored files by default
timeoutMs?: number, // Optional timeout in milliseconds (default: 30 seconds)
): Promise<string> {
const args = ["--json", "-e", regex, "--glob", filePattern || "*", "--context", "1", directoryPath]
if (!includesHiddenFiles) {
args.push("--no-hidden")
}
let output: string
try {
output = await execRipgrep(args)
output = await execRipgrep(args, timeoutMs)
} catch (error) {
throw Error("Error calling ripgrep", { cause: error })
}
+1
View File
@@ -191,6 +191,7 @@ export interface ClineSayTool {
| "listFilesTopLevel"
| "listFilesRecursive"
| "listCodeDefinitionNames"
| "searchAgent"
| "searchFiles"
| "webFetch"
| "webSearch"
+76
View File
@@ -0,0 +1,76 @@
import { ApiHandler } from "@/core/api"
import { ClineStorageMessage } from "@/shared/messages"
export interface AgentContext {
filePaths: Set<string>
searchResults: Map<string, SearchResult>
fileContents: Map<string, string>
}
export interface SearchResult {
query: string
workspaceName?: string
workspaceResults: string
resultCount: number
success: boolean
}
export interface FileReadResult {
path: string
content: string
success: boolean
}
/**
* Represents actions extracted from an agent's response
*/
export interface AgentActions {
/** Tool calls to execute (e.g., search queries, file reads) */
toolCalls: unknown[]
/** Context files the agent wants to use in the final answer */
contextFiles: string[]
/** Whether the agent is ready to provide a final answer */
isReadyToAnswer: boolean
}
/**
* Progress update sent during agent iteration
*/
export interface AgentIterationUpdate {
/** Current iteration number (0-indexed) */
iteration: number
/** Maximum iterations allowed */
maxIterations: number
/** Actions extracted from the agent's response */
actions?: AgentActions
/** Current context state */
context?: unknown
/** Cost incurred in this iteration */
cost?: number
/** Message describing the current status etc */
message?: string
}
/**
* Configuration for creating a ClineAgent instance
*/
export interface ClineAgentConfig {
/** Model ID to use (e.g., "x-ai/grok-code-fast-1") */
modelId: string
/** Maximum number of iterations in the agentic loop */
maxIterations?: number
/** Callback for iteration progress updates */
onIterationUpdate: (update: AgentIterationUpdate) => void | Promise<void>
/** System Prompt for the agent */
systemPrompt?: string
/** Starting messages for the agent */
messages?: ClineStorageMessage[]
/** API Request Params */
apiParams?: Record<string, unknown>
/** Optional API client to use instead of the default ClineHandler */
client?: ApiHandler
/** Optional tag name for context files extraction (default: no context extraction) */
contextTag?: string
/** Optional tag name for ready-to-answer check (default: no ready check) */
answerTag?: string
}
+1
View File
@@ -13,6 +13,7 @@ export enum ClineDefaultTool {
FILE_READ = "read_file",
FILE_NEW = "write_to_file",
SEARCH = "search_files",
SEARCH_AGENT = "search_subagent",
LIST_FILES = "list_files",
LIST_CODE_DEF = "list_code_definition_names",
BROWSER = "browser_action",
+28 -1
View File
@@ -12,7 +12,7 @@ import {
import { BooleanRequest, Int64Request, StringRequest } from "@shared/proto/cline/common"
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import { FoldVerticalIcon } from "lucide-react"
import { FoldVerticalIcon, Loader2Icon, ScanSearchIcon } from "lucide-react"
import React, { MouseEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import styled from "styled-components"
@@ -37,6 +37,7 @@ import { cn } from "@/lib/utils"
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import { Button } from "../ui/button"
import { DiffEditRow } from "./DiffEditRow"
import { ErrorBlockTitle } from "./ErrorBlockTitle"
import ErrorRow from "./ErrorRow"
@@ -780,6 +781,32 @@ export const ChatRowContent = memo(
/>
</>
)
case "searchAgent":
return (
<>
<div style={headerStyle}>
{message.partial ? (
<Loader2Icon className="size-2 animate-spin" />
) : (
<ScanSearchIcon className="size-2" />
)}
{tool.operationIsLocatedInWorkspace === false &&
toolIcon("sign-out", "yellow", -90, "This is outside of your workspace")}
<span className="font-bold">Cline wants to ask search agent:</span>
</div>
<Button
className="bg-code-block-background text-description border border-editor-group-border rounded-xs overflow-hidden w-full flex flex-col justify-start items-start text-left"
onClick={handleToggle}
variant="ghost">
{/* Search Agent Query */}
<span className="w-full break-words whitespace-normal text-left">{tool.filePattern} </span>
<div className="w-full flex flex-col gap-1 text-left select-text pt-1">
{(message.partial || isExpanded) &&
tool.content?.split("\n")?.map((line) => <div className="w-full">{line}</div>)}
</div>
</Button>
</>
)
case "searchFiles":
return (
<>
@@ -344,9 +344,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
Workspace
</span>
</VSCodeRadio>
<VSCodeRadio
checked={showFavoritesOnly}
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}>
<VSCodeRadio checked={showFavoritesOnly} onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}>
<span className="flex items-center gap-[3px]">
<span className="codicon codicon-star-full text-(--vscode-button-background)" />
Favorites