Compare commits

...
5 changed files with 419 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add newly formatted LS tool
@@ -473,3 +473,355 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
return contentBlocks
}
export function parseAssistantMessageV3(assistantMessage: string): AssistantMessageContent[] {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContentStart = 0 // Index where the current text block started
let currentTextContent: TextContent | undefined = undefined
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
let currentToolUse: ToolUse | undefined = undefined
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
let currentParamName: ToolParamName | undefined = undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
}
for (const name of toolParamNames) {
toolParamOpenTags.set(`<${name}>`, name)
}
// Function calls format detection
const isFunctionCallsOpen = "<function_calls>"
const isFunctionCallsClose = "</function_calls>"
const isInvokeStart = '<invoke name="'
const isInvokeEnd = '">'
const isInvokeClose = "</invoke>"
const isParameterStart = '<parameter name="'
const isParameterNameEnd = '">'
const isParameterClose = "</parameter>"
// Variables for function calls parsing
let inFunctionCalls = false
let currentInvokeName = ""
let currentParameterName = ""
const len = assistantMessage.length
for (let i = 0; i < len; i++) {
const currentCharIndex = i
// --- State: Parsing Function Calls ---
// Check for opening function_calls tag
if (
!inFunctionCalls &&
currentCharIndex >= isFunctionCallsOpen.length - 1 &&
assistantMessage.startsWith(isFunctionCallsOpen, currentCharIndex - isFunctionCallsOpen.length + 1)
) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart, currentCharIndex - isFunctionCallsOpen.length + 1)
.trim()
currentTextContent.partial = false
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
}
inFunctionCalls = true
continue
}
// Check for invoke start within function_calls
if (
inFunctionCalls &&
currentInvokeName === "" &&
currentCharIndex >= isInvokeStart.length - 1 &&
assistantMessage.startsWith(isInvokeStart, currentCharIndex - isInvokeStart.length + 1)
) {
// Find the end of the invoke name
const nameEndPos = assistantMessage.indexOf(isInvokeEnd, currentCharIndex + 1)
if (nameEndPos !== -1) {
// Extract the invoke name
currentInvokeName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
i = nameEndPos + isInvokeEnd.length - 1 // Skip to after the '">
// If this is an LS invoke, create a list_files tool
if (currentInvokeName === "LS") {
currentToolUse = {
type: "tool_use",
name: "list_files",
params: {},
partial: true,
}
}
continue
}
}
// Check for parameter start within invoke
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentParameterName === "" &&
currentCharIndex >= isParameterStart.length - 1 &&
assistantMessage.startsWith(isParameterStart, currentCharIndex - isParameterStart.length + 1)
) {
// Find the end of the parameter name
const nameEndPos = assistantMessage.indexOf(isParameterNameEnd, currentCharIndex + 1)
if (nameEndPos !== -1) {
// Extract the parameter name
currentParameterName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
currentParamValueStart = nameEndPos + isParameterNameEnd.length
i = nameEndPos + isParameterNameEnd.length - 1 // Skip to after the '">'
continue
}
}
// Check for parameter end
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentParameterName !== "" &&
currentCharIndex >= isParameterClose.length - 1 &&
assistantMessage.startsWith(isParameterClose, currentCharIndex - isParameterClose.length + 1)
) {
// Extract parameter value
const value = assistantMessage.slice(currentParamValueStart, currentCharIndex - isParameterClose.length + 1).trim()
// Map parameter to tool params
if (currentToolUse && currentInvokeName === "LS" && currentParameterName === "path") {
currentToolUse.params["path"] = value
// Default recursive to false - only show top level
currentToolUse.params["recursive"] = "false"
}
currentParameterName = ""
continue
}
// Check for invoke end
if (
inFunctionCalls &&
currentInvokeName !== "" &&
currentCharIndex >= isInvokeClose.length - 1 &&
assistantMessage.startsWith(isInvokeClose, currentCharIndex - isInvokeClose.length + 1)
) {
// If we have a tool use from this invoke, finalize it
if (currentToolUse && currentInvokeName === "LS") {
currentToolUse.partial = false
contentBlocks.push(currentToolUse)
currentToolUse = undefined
}
currentInvokeName = ""
continue
}
// Check for function_calls end
if (
inFunctionCalls &&
currentCharIndex >= isFunctionCallsClose.length - 1 &&
assistantMessage.startsWith(isFunctionCallsClose, currentCharIndex - isFunctionCallsClose.length + 1)
) {
inFunctionCalls = false
currentTextContentStart = currentCharIndex + 1
continue
}
// Skip normal parsing when inside function_calls
if (inFunctionCalls) {
continue
}
// --- State: Parsing a Tool Parameter ---
if (currentToolUse && currentParamName) {
const closeTag = `</${currentParamName}>`
// Check if the string *ending* at index `i` matches the closing tag
if (
currentCharIndex >= closeTag.length - 1 &&
assistantMessage.startsWith(
closeTag,
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
)
) {
// Found the closing tag for the parameter
const value = assistantMessage
.slice(
currentParamValueStart, // Start after the opening tag
currentCharIndex - closeTag.length + 1, // End before the closing tag
)
.trim()
currentToolUse.params[currentParamName] = value
currentParamName = undefined // Go back to parsing tool content
// We don't continue loop here, need to check for tool close or other params at index i
} else {
continue // Still inside param value, move to next char
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
if (currentToolUse && !currentParamName) {
// Ensure we are not inside a parameter already
// Check if starting a new parameter
let startedNewParam = false
for (const [tag, paramName] of toolParamOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
currentParamName = paramName
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
startedNewParam = true
break
}
}
if (startedNewParam) {
continue // Handled start of param, move to next char
}
// Check if closing the current tool use
const toolCloseTag = `</${currentToolUse.name}>`
if (
currentCharIndex >= toolCloseTag.length - 1 &&
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
) {
// End of the tool use found
// Special handling for content params *before* finalizing the tool
const toolContentSlice = assistantMessage.slice(
currentToolUseStart, // From after the tool opening tag
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
)
// Check if content parameter needs special handling (write_to_file/new_rule)
// This check is important if the closing </content> tag was missed by the parameter parsing logic
// (e.g., if content is empty or parsing logic prioritizes tool close)
const contentParamName: ToolParamName = "content"
if (
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
toolContentSlice.includes(`<${contentParamName}>`)
) {
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStart = toolContentSlice.indexOf(contentStartTag)
// Use lastIndexOf for robustness against nested tags
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
currentToolUse.params[contentParamName] = contentValue
}
}
currentToolUse.partial = false // Mark as complete
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Reset state
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
continue // Move to next char
}
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
continue
}
// --- State: Parsing Text / Looking for Tool Start ---
if (!currentToolUse) {
// Check if starting a new tool use
let startedNewTool = false
for (const [tag, toolName] of toolUseOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(
currentTextContentStart, // From where text started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
currentTextContent.partial = false // Ended because tool started
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
} else {
// Check for any text between the last block and this tag
const potentialText = assistantMessage
.slice(
currentTextContentStart, // From where text *might* have started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false,
})
}
}
// Start the new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
params: {},
partial: true, // Assume partial until closing tag is found
}
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
startedNewTool = true
break
}
}
if (startedNewTool) {
continue // Handled start of tool, move to next char
}
// If not starting a tool, it must be text content
if (!currentTextContent) {
// Start a new text block if we aren't already in one
currentTextContentStart = currentCharIndex // Text starts at the current character
// Check if the current char is the start of potential text *immediately* after a tag
// This needs the previous state - simpler to let slicing handle it later.
// Resetting start index accurately is key.
// It should be the index *after* the last processed tag.
// The logic managing currentTextContentStart after closing tags handles this.
currentTextContent = {
type: "text",
content: "", // Will be determined by slicing at the end or when a tool starts
partial: true,
}
}
// Continue accumulating text implicitly; content is extracted later.
}
} // End of loop
// --- Finalization after loop ---
// Finalize any open parameter within an open tool use
if (currentToolUse && currentParamName) {
currentToolUse.params[currentParamName] = assistantMessage
.slice(currentParamValueStart) // From param start to end of string
.trim()
// Tool use remains partial
}
// Finalize any open tool use (which might contain the finalized partial param)
if (currentToolUse) {
// Tool use is partial because the loop finished before its closing tag
contentBlocks.push(currentToolUse)
}
// Finalize any trailing text content
// Only possible if a tool use wasn't open at the very end
else if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart) // From text start to end of string
.trim()
// Text is partial because the loop finished
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
}
return contentBlocks
}
+5 -19
View File
@@ -8,10 +8,7 @@ import { createAntmlToolPrompt, createSimpleXmlToolPrompt, toolDefinitionToSimpl
import { bashToolDefinition } from "@core/tools/bashTool"
import { readToolDefinition } from "@core/tools/readTool"
import { writeToolDefinition } from "@core/tools/writeTool"
import { read } from "fs"
import {lsToolDefinition} from "@core/tools/lsTool"
export const SYSTEM_PROMPT_CLAUDE4 = async (
cwd: string,
@@ -132,17 +129,6 @@ Usage:
<file_pattern>file pattern here (optional)</file_pattern>
</search_files>
## list_files
Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
Parameters:
- path: (required) The path of the directory to list contents for (relative to the current working directory ${cwd.toPosix()})
- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
Usage:
<list_files>
<path>Directory path here</path>
<recursive>true or false (optional)</recursive>
</list_files>
## list_code_definition_names
Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.
Parameters:
@@ -398,7 +384,7 @@ Usage:
# Tool Use Guidelines
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. It's critical that you think about each available tool and use the one that best fits the current step in the task.
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
4. Formulate your tool use using the XML format specified for each tool.
5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
@@ -570,7 +556,7 @@ CAPABILITIES
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search${
supportsBrowserUse ? ", use the browser" : ""
}, read and edit files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
- When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('${cwd.toPosix()}') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
- When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('${cwd.toPosix()}') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
@@ -596,7 +582,7 @@ RULES
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the LS tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
@@ -638,5 +624,5 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
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. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
return createAntmlToolPrompt([bashToolDefinition, readToolDefinition, writeToolDefinition], true, systemPrompt);
return createAntmlToolPrompt([lsToolDefinition], true, systemPrompt);
}
+40 -21
View File
@@ -104,6 +104,7 @@ import { isInTestMode } from "../../services/test/TestMode"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { featureFlagsService } from "@services/posthog/feature-flags/FeatureFlagsService"
import { StreamingJsonReplacer, ChangeLocation } from "@core/assistant-message/diff-json"
import { parseAssistantMessageV3 } from "../assistant-message/parse-assistant-message"
export 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
@@ -1590,7 +1591,7 @@ export class Task {
private async isClaude4ModelFamily(): Promise<boolean> {
const model = this.api.getModel()
const modelId = model.id
return modelId.includes("claude-sonnet-4") || modelId.includes("claude-opus-4")
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
@@ -2016,16 +2017,27 @@ export class Task {
break
}
const pushToolResult = (content: ToolResponse) => {
this.userMessageContent.push({
type: "text",
text: `${toolDescription()} Result:`,
})
const pushToolResult = (content: ToolResponse, isClaude4ModelFamily: boolean = false) => {
if (typeof content === "string") {
this.userMessageContent.push({
type: "text",
text: content || "(tool did not return anything)",
})
const resultText = content || "(tool did not return anything)"
if (isClaude4ModelFamily) {
// Claude 4 family: Use function_results format
this.userMessageContent.push({
type: "text",
text: `<function_results>\n${resultText}\n</function_results>`,
})
} else {
// Non-Claude 4: Use traditional format with header
this.userMessageContent.push({
type: "text",
text: `${toolDescription()} Result:`,
})
this.userMessageContent.push({
type: "text",
text: resultText,
})
}
} else {
this.userMessageContent.push(...content)
}
@@ -2095,7 +2107,7 @@ export class Task {
}
}
const handleError = async (action: string, error: Error) => {
const handleError = async (action: string, error: Error, isClaude4ModelFamily: boolean = false) => {
if (this.abandoned) {
console.log("Ignoring error since task was abandoned (i.e. from task cancellation after resetting)")
return
@@ -2105,12 +2117,8 @@ export class Task {
"error",
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
)
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: await this.formatToolError(errorString),
// })
pushToolResult(formatResponse.toolError(errorString))
pushToolResult(formatResponse.toolError(errorString), isClaude4ModelFamily)
}
// If block is partial, remove partial closing tag so its not presented to user
@@ -2558,6 +2566,7 @@ export class Task {
}
}
case "list_files": {
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
const relDirPath: string | undefined = block.params.path
const recursiveRaw: string | undefined = block.params.recursive
const recursive = recursiveRaw?.toLowerCase() === "true"
@@ -2583,7 +2592,10 @@ export class Task {
} else {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
pushToolResult(
await this.sayAndCreateMissingParamError("list_files", "path"),
isClaude4ModelFamily,
)
await this.saveCheckpoint()
break
}
@@ -2622,12 +2634,12 @@ export class Task {
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
pushToolResult(result, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error)
await handleError("listing files", error, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
@@ -4256,7 +4268,14 @@ export class Task {
assistantMessage += chunk.text
// parse raw assistant message into content blocks
const prevLength = this.assistantMessageContent.length
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
const enableFunctionCallsParsing = await this.isClaude4ModelFamily()
if (enableFunctionCallsParsing) {
this.assistantMessageContent = parseAssistantMessageV3(assistantMessage)
} else {
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
}
if (this.assistantMessageContent.length > prevLength) {
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
}
+17
View File
@@ -0,0 +1,17 @@
import { ToolDefinition } from "@core/prompts/model_prompts/jsonToolToXml"
export const lsToolDefinition: ToolDefinition = {
name: "LS",
descriptionForAgent:
"Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You should generally prefer the Glob and Grep tools, if you know which directories to search.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "The path of the directory to list contents for",
},
},
required: ["path"],
},
}