From 6e1dfb7377d7405a1aefccd655d7293aa3f1d629 Mon Sep 17 00:00:00 2001 From: Yevhen Mohylevskyy Date: Thu, 24 Jul 2025 13:59:52 -0700 Subject: [PATCH 01/20] add multi edit tool --- package.json | 52 ++++++ package.nls.json | 1 + .../vscode-node/languageModelAccess.ts | 6 +- .../prompts/node/panel/toolCalling.tsx | 27 ++++ src/extension/tools/common/toolNames.ts | 3 + .../tools/common/toolSchemaNormalizer.ts | 23 +++ src/extension/tools/node/allTools.ts | 1 + .../tools/node/multiReplaceStringTool.tsx | 148 ++++++++++++++++++ .../addNextToolPredictionParameter.spec.ts | 0 9 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 src/extension/tools/node/multiReplaceStringTool.tsx create mode 100644 src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts diff --git a/package.json b/package.json index 523ec5425..acdf9e8b5 100644 --- a/package.json +++ b/package.json @@ -889,6 +889,58 @@ ] } }, + { + "name": "copilot_multiReplaceString", + "toolReferenceName": "multiReplaceString", + "displayName": "%copilot.tools.multiReplaceString.name%", + "modelDescription": "This tool allows you to apply multiple replace_string_in_file operations in a single call, which is more efficient than calling replace_string_in_file multiple times. It takes an array of replacement operations and applies them sequentially. Each replacement operation has the same parameters as replace_string_in_file: filePath, oldString, newString, and explanation. This tool is ideal when you need to make multiple edits across different files or multiple edits in the same file. The tool will provide a summary of successful and failed operations.", + "when": "!config.github.copilot.chat.disableReplaceTool", + "inputSchema": { + "type": "object", + "properties": { + "explanation": { + "type": "string", + "description": "A brief explanation of what the multi-replace operation will accomplish." + }, + "replacements": { + "type": "array", + "description": "An array of replacement operations to apply sequentially.", + "items": { + "type": "object", + "properties": { + "explanation": { + "type": "string", + "description": "A brief explanation of this specific replacement operation." + }, + "filePath": { + "type": "string", + "description": "An absolute path to the file to edit." + }, + "oldString": { + "type": "string", + "description": "The exact literal text to replace, preferably unescaped. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string is not the exact literal text or does not match exactly, this replacement will fail." + }, + "newString": { + "type": "string", + "description": "The exact literal text to replace `oldString` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic." + } + }, + "required": [ + "explanation", + "filePath", + "oldString", + "newString" + ] + }, + "minItems": 1 + } + }, + "required": [ + "explanation", + "replacements" + ] + } + }, { "name": "copilot_editNotebook", "toolReferenceName": "editNotebook", diff --git a/package.nls.json b/package.nls.json index a04a95450..7bbdf0de9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -274,6 +274,7 @@ "copilot.tools.createFile.name": "Create File", "copilot.tools.insertEdit.name": "Edit File", "copilot.tools.replaceString.name": "Replace String in File", + "copilot.tools.multiReplaceString.name": "Multi-Replace String in Files", "copilot.tools.editNotebook.name": "Edit Notebook", "copilot.tools.runNotebookCell.name": "Run Notebook Cell", "copilot.tools.getNotebookCellOutput.name": "Get Notebook Cell Output", diff --git a/src/extension/conversation/vscode-node/languageModelAccess.ts b/src/extension/conversation/vscode-node/languageModelAccess.ts index e5450bf4b..c3f79bdce 100644 --- a/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -132,17 +132,13 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } const baseCount = await PromptRenderer.create(this._instantiationService, endpoint, LanguageModelAccessPrompt, { noSafety: false, messages: [] }).countTokens(); - let multiplierString = endpoint.multiplier !== undefined ? `${endpoint.multiplier}x` : undefined; - if (endpoint.model === AutoChatEndpoint.id) { - multiplierString = 'Variable'; - } const model: vscode.LanguageModelChatInformation = { id: endpoint.model, name: endpoint.name, family: endpoint.family, description: modelDescription, - cost: multiplierString, + cost: endpoint.multiplier !== undefined && endpoint.multiplier !== 0 ? `${endpoint.multiplier}x` : endpoint.multiplier === 0 ? localize('languageModel.costIncluded', 'Included') : undefined, category: modelCategory, version: endpoint.version, maxInputTokens: endpoint.modelMaxPromptTokens - baseCount - BaseTokensPerCompletion, diff --git a/src/extension/prompts/node/panel/toolCalling.tsx b/src/extension/prompts/node/panel/toolCalling.tsx index 117cf0496..aa849ce5a 100644 --- a/src/extension/prompts/node/panel/toolCalling.tsx +++ b/src/extension/prompts/node/panel/toolCalling.tsx @@ -207,6 +207,32 @@ class ToolResultElement extends PromptElement { } } this.sendToolCallTelemetry(outcome, validation); + } // Check if next_tool_prediction contains "replace_string_in_file" and prepare reminder text + let reminderText = ''; + if (toolResult) { + try { + const toolArgs = JSON.parse(this.props.toolCall.arguments); + this.logService.logger.info(`Tool call arguments parsed: ${JSON.stringify(toolArgs)}`); + + if (toolArgs.next_tool_prediction && Array.isArray(toolArgs.next_tool_prediction)) { + this.logService.logger.info(`Found next_tool_prediction: ${JSON.stringify(toolArgs.next_tool_prediction)}`); + const hasSomeTool = toolArgs.next_tool_prediction.some((tool: string) => + typeof tool === 'string' && tool.includes('replace_string_in_file') + ); + + if (hasSomeTool) { + this.logService.logger.info('Found "replace_string_in_file" in next_tool_prediction, adding reminder'); + reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; + } else { + this.logService.logger.info('No "replace_string_in_file" found in next_tool_prediction'); + } + } else { + this.logService.logger.info('No next_tool_prediction found or not an array'); + } + } catch (error) { + // If parsing fails, continue with original result + this.logService.logger.warn('Failed to parse tool arguments for next_tool_prediction check'); + } } const toolResultElement = this.props.enableCacheBreakpoints ? @@ -222,6 +248,7 @@ class ToolResultElement extends PromptElement { {...extraMetadata.map(m => )} {toolResultElement} + {reminderText &&
{reminderText}
} {this.props.isLast && this.props.enableCacheBreakpoints && } ); diff --git a/src/extension/tools/common/toolNames.ts b/src/extension/tools/common/toolNames.ts index 375987f37..d90245ef5 100644 --- a/src/extension/tools/common/toolNames.ts +++ b/src/extension/tools/common/toolNames.ts @@ -29,6 +29,7 @@ export const enum ToolName { EditFile = 'insert_edit_into_file', CreateFile = 'create_file', ReplaceString = 'replace_string_in_file', + MultiReplaceString = 'multi_replace_string_in_file', EditNotebook = 'edit_notebook_file', RunNotebookCell = 'run_notebook_cell', GetNotebookSummary = 'copilot_getNotebookSummary', @@ -80,6 +81,7 @@ export const enum ContributedToolName { EditFile = 'copilot_insertEdit', CreateFile = 'copilot_createFile', ReplaceString = 'copilot_replaceString', + MultiReplaceString = 'copilot_multiReplaceString', EditNotebook = 'copilot_editNotebook', RunNotebookCell = 'copilot_runNotebookCell', GetNotebookSummary = 'copilot_getNotebookSummary', @@ -126,6 +128,7 @@ const contributedToolNameToToolNames = new Map([ [ContributedToolName.FindTestFiles, ToolName.FindTestFiles], [ContributedToolName.CreateFile, ToolName.CreateFile], [ContributedToolName.ReplaceString, ToolName.ReplaceString], + [ContributedToolName.MultiReplaceString, ToolName.MultiReplaceString], [ContributedToolName.EditNotebook, ToolName.EditNotebook], [ContributedToolName.RunNotebookCell, ToolName.RunNotebookCell], [ContributedToolName.GetNotebookSummary, ToolName.GetNotebookSummary], diff --git a/src/extension/tools/common/toolSchemaNormalizer.ts b/src/extension/tools/common/toolSchemaNormalizer.ts index ac9e4aea4..2c85c442a 100644 --- a/src/extension/tools/common/toolSchemaNormalizer.ts +++ b/src/extension/tools/common/toolSchemaNormalizer.ts @@ -76,6 +76,29 @@ const fnRules: ((family: string, node: OpenAiFunctionDef, didFix: (message: stri didFix('schema description may not be empty'); } }, + // Add next_tool_prediction to all tools for informational purposes + (_family, n, _didFix) => { + if (n.parameters && (n.parameters as ObjectJsonSchema).type === 'object') { + const obj = n.parameters as ObjectJsonSchema; + if (obj.properties && !obj.properties.next_tool_prediction) { + obj.properties.next_tool_prediction = { + description: 'Provide a short list of tools you are most likely to use next. You do not have to follow your prediction, but it would still be helpful.', + type: 'array', + items: { + type: 'string' + } + }; + + // Add to required array + if (!obj.required) { + obj.required = []; + } + if (!obj.required.includes('next_tool_prediction')) { + obj.required.push('next_tool_prediction'); + } + } + } + }, ]; const ajvJsonValidator = new Lazy(() => { diff --git a/src/extension/tools/node/allTools.ts b/src/extension/tools/node/allTools.ts index 189725acd..2c199026c 100644 --- a/src/extension/tools/node/allTools.ts +++ b/src/extension/tools/node/allTools.ts @@ -20,6 +20,7 @@ import './githubRepoTool'; import './insertEditTool'; import './installExtensionTool'; import './listDirTool'; +import './multiReplaceStringTool'; import './newNotebookTool'; import './newWorkspace/createAndRunTaskTool'; import './newWorkspace/newWorkspaceTool'; diff --git a/src/extension/tools/node/multiReplaceStringTool.tsx b/src/extension/tools/node/multiReplaceStringTool.tsx new file mode 100644 index 000000000..fe5ef835f --- /dev/null +++ b/src/extension/tools/node/multiReplaceStringTool.tsx @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; +import { LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes'; +import { IBuildPromptContext } from '../../prompt/common/intents'; +import { ToolName } from '../common/toolNames'; +import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; +import { IToolsService } from '../common/toolsService'; +import { IReplaceStringToolParams, ReplaceStringTool } from './replaceStringTool'; + +export interface IMultiReplaceStringToolParams { + explanation: string; + replacements: IReplaceStringToolParams[]; +} + +export interface IMultiReplaceResult { + totalReplacements: number; + successfulReplacements: number; + failedReplacements: number; + results: Array<{ + operation: IReplaceStringToolParams; + success: boolean; + error?: string; + }>; +} + +export class MultiReplaceStringTool implements ICopilotTool { + public static toolName = ToolName.MultiReplaceString; + + private _promptContext: IBuildPromptContext | undefined; + + constructor( + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IToolsService protected readonly toolsService: IToolsService + ) { } + + // Simplified version that uses a more direct approach + async invoke(options: any, token: any) { + // Cast the options to the correct type to work around TypeScript issues + const typedOptions = options as vscode.LanguageModelToolInvocationOptions & { input: IMultiReplaceStringToolParams }; + + // Validate input + if (!typedOptions.input.replacements || !Array.isArray(typedOptions.input.replacements) || typedOptions.input.replacements.length === 0) { + throw new Error('Invalid input: replacements array is required and must contain at least one replacement operation'); + } + + if (!this._promptContext?.stream) { + throw new Error('Invalid context: stream is required'); + } + + const results: IMultiReplaceResult = { + totalReplacements: typedOptions.input.replacements.length, + successfulReplacements: 0, + failedReplacements: 0, + results: [] + }; + + // Get the ReplaceStringTool instance + const replaceStringTool = this.instantiationService.createInstance(ReplaceStringTool); + + // Apply replacements sequentially + for (let i = 0; i < typedOptions.input.replacements.length; i++) { + const replacement = typedOptions.input.replacements[i]; + + try { + // Validate individual replacement + if (!replacement.filePath || replacement.oldString === undefined || replacement.newString === undefined) { + throw new Error(`Invalid replacement at index ${i}: filePath, oldString, and newString are required`); + } + + // Create a new tool invocation options for this replacement + const replaceOptions = { + ...typedOptions, + input: replacement + }; + + // Set the prompt context for the replace tool + await replaceStringTool.resolveInput(replacement, this._promptContext); + + // Invoke the replace string tool + await replaceStringTool.invoke(replaceOptions as any, token); + + // Record success + results.results.push({ + operation: replacement, + success: true + }); + results.successfulReplacements++; + + } catch (error) { + // Record failure + const errorMessage = error instanceof Error ? error.message : String(error); + results.results.push({ + operation: replacement, + success: false, + error: errorMessage + }); + results.failedReplacements++; + + // Add error information to the stream using the correct method + (this._promptContext.stream as any).markdown(`\nāš ļø **Failed replacement ${i + 1}:**\n`); + (this._promptContext.stream as any).markdown(`- File: \`${replacement.filePath}\`\n`); + (this._promptContext.stream as any).markdown(`- Error: ${errorMessage}\n\n`); + } + } + + // Provide summary using the correct method + (this._promptContext.stream as any).markdown(`\n## Multi-Replace Summary\n\n`); + (this._promptContext.stream as any).markdown(`- **Total operations:** ${results.totalReplacements}\n`); + (this._promptContext.stream as any).markdown(`- **Successful:** ${results.successfulReplacements}\n`); + (this._promptContext.stream as any).markdown(`- **Failed:** ${results.failedReplacements}\n\n`); + + if (results.failedReplacements > 0) { + (this._promptContext.stream as any).markdown(`### Failed Operations:\n\n`); + results.results.filter(r => !r.success).forEach((result, index) => { + if (this._promptContext?.stream) { + (this._promptContext.stream as any).markdown(`${index + 1}. **${result.operation.filePath}**\n`); + (this._promptContext.stream as any).markdown(` - Error: ${result.error || 'Unknown error'}\n`); + (this._promptContext.stream as any).markdown(` - Old string: \`${result.operation.oldString.substring(0, 100)}${result.operation.oldString.length > 100 ? '...' : ''}\`\n\n`); + } + }); + } + + // Return a simple result + return new LanguageModelToolResult([ + new LanguageModelTextPart( + `Multi-replace operation completed: ${results.successfulReplacements}/${results.totalReplacements} operations successful.` + ) + ]); + } + + async resolveInput(input: IMultiReplaceStringToolParams, promptContext: IBuildPromptContext): Promise { + this._promptContext = promptContext; + return input; + } + + prepareInvocation(options: any, token: any): any { + return { + presentation: 'hidden' + }; + } +} + +ToolRegistry.registerTool(MultiReplaceStringTool); diff --git a/src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts b/src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts new file mode 100644 index 000000000..e69de29bb From f4a0a6a3043b21a93cf5c9f1b1f7e34d09876bf4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 24 Jul 2025 22:16:50 +0000 Subject: [PATCH 02/20] comment out reminder --- src/extension/prompts/node/panel/toolCalling.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extension/prompts/node/panel/toolCalling.tsx b/src/extension/prompts/node/panel/toolCalling.tsx index aa849ce5a..6fc4244b9 100644 --- a/src/extension/prompts/node/panel/toolCalling.tsx +++ b/src/extension/prompts/node/panel/toolCalling.tsx @@ -222,7 +222,8 @@ class ToolResultElement extends PromptElement { if (hasSomeTool) { this.logService.logger.info('Found "replace_string_in_file" in next_tool_prediction, adding reminder'); - reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; + //reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; + reminderText = ''; } else { this.logService.logger.info('No "replace_string_in_file" found in next_tool_prediction'); } @@ -248,7 +249,7 @@ class ToolResultElement extends PromptElement { {...extraMetadata.map(m => )} {toolResultElement} - {reminderText &&
{reminderText}
} + {reminderText && reminderText} {this.props.isLast && this.props.enableCacheBreakpoints && } ); From 26969ceb54735ce63f0785f2cfe19ce460c59adf Mon Sep 17 00:00:00 2001 From: Yevhen Mohylevskyy Date: Thu, 24 Jul 2025 15:21:45 -0700 Subject: [PATCH 03/20] comment out reminder --- IMPLEMENTATION_SUMMARY.md | 0 .../prompts/node/panel/toolCalling.tsx | 5 +- test_dummy_parameter.js | 87 +++++++++++++++++++ test_implementation.js | 0 test_next_tool_prediction.js | 0 test_required_dummy.js | 0 verify_implementation.js | 0 7 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 test_dummy_parameter.js create mode 100644 test_implementation.js create mode 100644 test_next_tool_prediction.js create mode 100644 test_required_dummy.js create mode 100644 verify_implementation.js diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/extension/prompts/node/panel/toolCalling.tsx b/src/extension/prompts/node/panel/toolCalling.tsx index aa849ce5a..6fc4244b9 100644 --- a/src/extension/prompts/node/panel/toolCalling.tsx +++ b/src/extension/prompts/node/panel/toolCalling.tsx @@ -222,7 +222,8 @@ class ToolResultElement extends PromptElement { if (hasSomeTool) { this.logService.logger.info('Found "replace_string_in_file" in next_tool_prediction, adding reminder'); - reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; + //reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; + reminderText = ''; } else { this.logService.logger.info('No "replace_string_in_file" found in next_tool_prediction'); } @@ -248,7 +249,7 @@ class ToolResultElement extends PromptElement { {...extraMetadata.map(m => )} {toolResultElement} - {reminderText &&
{reminderText}
} + {reminderText && reminderText} {this.props.isLast && this.props.enableCacheBreakpoints && } ); diff --git a/test_dummy_parameter.js b/test_dummy_parameter.js new file mode 100644 index 000000000..e844eee24 --- /dev/null +++ b/test_dummy_parameter.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +/** + * Test script to verify dummy_parameter is added to tools + */ + +// Mock the dependencies for testing +const mockDeepClone = (obj) => JSON.parse(JSON.stringify(obj)); + +// Simplified version of the rule we added +function addDummyParameterToTool(tool) { + const cloned = mockDeepClone(tool); + + // Apply the dummy parameter rule + if (cloned.function.parameters && cloned.function.parameters.type === 'object') { + const obj = cloned.function.parameters; + if (obj.properties && !obj.properties.dummy_parameter) { + obj.properties.dummy_parameter = { + type: 'string', + description: 'Dummy parameter for informational purposes only. Does not affect tool behavior.', + default: 'dummy_value' + }; + } + } + + return cloned; +} + +// Test the implementation +console.log('šŸ”§ Testing dummy_parameter addition...\n'); + +// Test 1: Tool with existing parameters +console.log('Test 1: Tool with existing parameters'); +const tool1 = { + type: 'function', + function: { + name: 'testTool', + description: 'A test tool', + parameters: { + type: 'object', + properties: { + input: { type: 'string' } + } + } + } +}; + +const result1 = addDummyParameterToTool(tool1); +console.log('āœ“ Dummy parameter added:', !!result1.function.parameters.properties.dummy_parameter); +console.log('āœ“ Original parameters preserved:', !!result1.function.parameters.properties.input); +console.log('āœ“ Dummy parameter description:', result1.function.parameters.properties.dummy_parameter?.description); +console.log(''); + +// Test 2: Tool with no parameters (will be skipped) +console.log('Test 2: Tool with no parameters'); +const tool2 = { + type: 'function', + function: { + name: 'noParamTool', + description: 'A tool with no parameters' + } +}; + +const result2 = addDummyParameterToTool(tool2); +console.log('āœ“ No parameters added (as expected):', !result2.function.parameters); +console.log(''); + +// Test 3: Tool with parameters but no properties +console.log('Test 3: Tool with parameters but no properties'); +const tool3 = { + type: 'function', + function: { + name: 'emptyParamTool', + description: 'A tool with empty parameters', + parameters: { + type: 'object' + } + } +}; + +const result3 = addDummyParameterToTool(tool3); +console.log('āœ“ No dummy parameter added (no properties object):', !result3.function.parameters.properties?.dummy_parameter); +console.log(''); + +console.log('šŸŽ‰ All tests completed! The rule works as expected.'); +console.log('\nšŸ“ Note: The actual implementation in toolSchemaNormalizer.ts will automatically'); +console.log(' apply this rule to all tools processed through normalizeToolSchema().'); diff --git a/test_implementation.js b/test_implementation.js new file mode 100644 index 000000000..e69de29bb diff --git a/test_next_tool_prediction.js b/test_next_tool_prediction.js new file mode 100644 index 000000000..e69de29bb diff --git a/test_required_dummy.js b/test_required_dummy.js new file mode 100644 index 000000000..e69de29bb diff --git a/verify_implementation.js b/verify_implementation.js new file mode 100644 index 000000000..e69de29bb From 666791b204721b43be17d67648ba6a23db2dc8ba Mon Sep 17 00:00:00 2001 From: Yevhen Mohylevskyy Date: Thu, 24 Jul 2025 15:35:50 -0700 Subject: [PATCH 04/20] fix multi_replace_string_in_file issue in intents --- src/extension/intents/node/agentIntent.ts | 2 ++ src/extension/intents/node/editCodeIntent2.ts | 1 + src/extension/intents/node/notebookEditorIntent.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/src/extension/intents/node/agentIntent.ts b/src/extension/intents/node/agentIntent.ts index 46e4f555a..cbe81063d 100644 --- a/src/extension/intents/node/agentIntent.ts +++ b/src/extension/intents/node/agentIntent.ts @@ -62,10 +62,12 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const allowTools: Record = {}; allowTools[ToolName.EditFile] = true; allowTools[ToolName.ReplaceString] = modelSupportsReplaceString(model) || !!(model.family.includes('gemini') && configurationService.getExperimentBasedConfig(ConfigKey.Internal.GeminiReplaceString, experimentationService)); + allowTools[ToolName.MultiReplaceString] = modelSupportsReplaceString(model) || !!(model.family.includes('gemini') && configurationService.getExperimentBasedConfig(ConfigKey.Internal.GeminiReplaceString, experimentationService)); allowTools[ToolName.ApplyPatch] = modelSupportsApplyPatch(model) && !!toolsService.getTool(ToolName.ApplyPatch); if (modelCanUseReplaceStringExclusively(model)) { allowTools[ToolName.ReplaceString] = true; + allowTools[ToolName.MultiReplaceString] = true; allowTools[ToolName.EditFile] = false; } diff --git a/src/extension/intents/node/editCodeIntent2.ts b/src/extension/intents/node/editCodeIntent2.ts index 78bb26547..6a6100006 100644 --- a/src/extension/intents/node/editCodeIntent2.ts +++ b/src/extension/intents/node/editCodeIntent2.ts @@ -48,6 +48,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque if (model.family.startsWith('claude')) { lookForTools.add(ToolName.ReplaceString); + lookForTools.add(ToolName.MultiReplaceString); } lookForTools.add(ToolName.EditNotebook); if (requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) { diff --git a/src/extension/intents/node/notebookEditorIntent.ts b/src/extension/intents/node/notebookEditorIntent.ts index dc0ad85b0..2501fe98d 100644 --- a/src/extension/intents/node/notebookEditorIntent.ts +++ b/src/extension/intents/node/notebookEditorIntent.ts @@ -49,6 +49,7 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque if (model.family.startsWith('claude')) { lookForTools.add(ToolName.ReplaceString); + lookForTools.add(ToolName.MultiReplaceString); } lookForTools.add(ToolName.EditNotebook); From 89ca44c9835a1147fdf5c8be16f8561137097e4c Mon Sep 17 00:00:00 2001 From: Yevhen Mohylevskyy Date: Thu, 24 Jul 2025 15:54:32 -0700 Subject: [PATCH 05/20] add multi_replace_string_in_file related instructions --- src/extension/prompts/node/agent/agentInstructions.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index 49a83e939..02bd66ad7 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -76,17 +76,19 @@ export class DefaultAgentPrompt extends PromptElement { <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
+ Use the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is more efficient than calling {ToolName.ReplaceString} multiple times. Ideal for: fixing similar typos across files, applying consistent formatting changes, or bulk refactoring operations.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.EditFile} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: + NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.
+ Use the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
+ NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.MultiReplaceString} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
} The {ToolName.EditFile} tool is very smart and can understand how to apply your edits to the user's files, you just need to provide minimal hints.
From 0065bdf3fbb0b6fdac6c46ab05d827d4b20f0281 Mon Sep 17 00:00:00 2001 From: Yevhen Mohylevskyy Date: Thu, 24 Jul 2025 16:20:07 -0700 Subject: [PATCH 06/20] add more instructions --- src/extension/prompts/node/agent/agentInstructions.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index 02bd66ad7..3b7fdfd6b 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -27,6 +27,7 @@ export class DefaultAgentPrompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const hasTerminalTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.CoreRunInTerminal); const hasReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ReplaceString); + const hasMultiReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.MultiReplaceString); const hasInsertEditTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.EditFile); const hasApplyPatchTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ApplyPatch); const hasReadFileTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ReadFile); @@ -75,17 +76,17 @@ export class DefaultAgentPrompt extends PromptElement { {hasReplaceStringTool ? <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
- Use the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is more efficient than calling {ToolName.ReplaceString} multiple times. Ideal for: fixing similar typos across files, applying consistent formatting changes, or bulk refactoring operations.
+ {hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
} + Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
- When editing files, group your changes by file.
+ When editing files, group your changes by file and consider whether {hasMultiReplaceStringTool && <>{ToolName.MultiReplaceString} or }{ToolName.ReplaceString} would be more efficient.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} instead.
For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
+ {hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This should be your first choice for bulk edits.
} Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.
- Use the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.MultiReplaceString} instead.
For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
From 1b3901542aff4cf96cb5e27bc342e1babd4bd6ba Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 21:34:51 +0000 Subject: [PATCH 07/20] remove next tool prediction parsing --- .../prompts/node/panel/toolCalling.tsx | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/extension/prompts/node/panel/toolCalling.tsx b/src/extension/prompts/node/panel/toolCalling.tsx index c9f4e06a8..700beb483 100644 --- a/src/extension/prompts/node/panel/toolCalling.tsx +++ b/src/extension/prompts/node/panel/toolCalling.tsx @@ -212,33 +212,6 @@ class ToolResultElement extends PromptElement { } } this.sendToolCallTelemetry(outcome, validation); - } // Check if next_tool_prediction contains "replace_string_in_file" and prepare reminder text - let reminderText = ''; - if (toolResult) { - try { - const toolArgs = JSON.parse(this.props.toolCall.arguments); - this.logService.logger.info(`Tool call arguments parsed: ${JSON.stringify(toolArgs)}`); - - if (toolArgs.next_tool_prediction && Array.isArray(toolArgs.next_tool_prediction)) { - this.logService.logger.info(`Found next_tool_prediction: ${JSON.stringify(toolArgs.next_tool_prediction)}`); - const hasSomeTool = toolArgs.next_tool_prediction.some((tool: string) => - typeof tool === 'string' && tool.includes('replace_string_in_file') - ); - - if (hasSomeTool) { - this.logService.logger.info('Found "replace_string_in_file" in next_tool_prediction, adding reminder'); - //reminderText = '\n\n\nIf you need to make multiple edits using replace_string_in_file tool, consider making them in parallel whenever possible.\n'; - reminderText = ''; - } else { - this.logService.logger.info('No "replace_string_in_file" found in next_tool_prediction'); - } - } else { - this.logService.logger.info('No next_tool_prediction found or not an array'); - } - } catch (error) { - // If parsing fails, continue with original result - this.logService.logger.warn('Failed to parse tool arguments for next_tool_prediction check'); - } } const toolResultElement = this.props.enableCacheBreakpoints ? @@ -254,7 +227,6 @@ class ToolResultElement extends PromptElement { {...extraMetadata.map(m => )} {toolResultElement} - {reminderText && reminderText} {this.props.isLast && this.props.enableCacheBreakpoints && } ); From 8317ef426ea4eb2a141da6ffefcc3f51918990dd Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 21:52:25 +0000 Subject: [PATCH 08/20] remove next_tool_prediction from toolSchemaNormalizer.ts --- .../tools/common/toolSchemaNormalizer.ts | 23 ------------------- test_next_tool_prediction.js | 0 2 files changed, 23 deletions(-) delete mode 100644 test_next_tool_prediction.js diff --git a/src/extension/tools/common/toolSchemaNormalizer.ts b/src/extension/tools/common/toolSchemaNormalizer.ts index 2c85c442a..ac9e4aea4 100644 --- a/src/extension/tools/common/toolSchemaNormalizer.ts +++ b/src/extension/tools/common/toolSchemaNormalizer.ts @@ -76,29 +76,6 @@ const fnRules: ((family: string, node: OpenAiFunctionDef, didFix: (message: stri didFix('schema description may not be empty'); } }, - // Add next_tool_prediction to all tools for informational purposes - (_family, n, _didFix) => { - if (n.parameters && (n.parameters as ObjectJsonSchema).type === 'object') { - const obj = n.parameters as ObjectJsonSchema; - if (obj.properties && !obj.properties.next_tool_prediction) { - obj.properties.next_tool_prediction = { - description: 'Provide a short list of tools you are most likely to use next. You do not have to follow your prediction, but it would still be helpful.', - type: 'array', - items: { - type: 'string' - } - }; - - // Add to required array - if (!obj.required) { - obj.required = []; - } - if (!obj.required.includes('next_tool_prediction')) { - obj.required.push('next_tool_prediction'); - } - } - } - }, ]; const ajvJsonValidator = new Lazy(() => { diff --git a/test_next_tool_prediction.js b/test_next_tool_prediction.js deleted file mode 100644 index e69de29bb..000000000 From cfb143db0d2b93d0d30f439b8ea9007937205009 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 22:13:33 +0000 Subject: [PATCH 09/20] add initial user reminder --- src/extension/prompts/node/agent/agentPrompt.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index 207da420d..675612034 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -283,6 +283,7 @@ export class AgentUserMessage extends PromptElement { const query = await this.promptVariablesService.resolveToolReferencesInPrompt(this.props.request, this.props.toolReferences ?? []); const hasReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ReplaceString); + const hasMultiReplaceStringTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.MultiReplaceString); const hasApplyPatchTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.ApplyPatch); const hasCreateFileTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.CreateFile); const hasEditFileTool = !!this.props.availableTools?.find(tool => tool.name === ToolName.EditFile); @@ -316,6 +317,7 @@ export class AgentUserMessage extends PromptElement { {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint))} {getExplanationReminder(this.props.endpoint.family, hasTodoTool)} + {hasMultiReplaceStringTool && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
} {query && {query + attachmentHint}} {this.props.enableCacheBreakpoints && } From 88af9948990b0312dbfac999464b3bc1788a1d55 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 22:27:36 +0000 Subject: [PATCH 10/20] add hasMultiReplaceString --- src/extension/prompts/node/agent/agentInstructions.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index 27f970634..9b31786ec 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -19,6 +19,7 @@ import { KeepGoingReminder } from './agentPrompt'; interface ToolCapabilities { hasTerminalTool: boolean; hasReplaceStringTool: boolean; + hasMultiReplaceStringTool: boolean; hasInsertEditTool: boolean; hasApplyPatchTool: boolean; hasReadFileTool: boolean; @@ -36,6 +37,7 @@ function detectToolCapabilities(availableTools: readonly LanguageModelToolInform return { hasTerminalTool: !!availableTools?.find(tool => tool.name === ToolName.CoreRunInTerminal) || !!toolsService?.getTool(ToolName.CoreRunInTerminal), hasReplaceStringTool: !!availableTools?.find(tool => tool.name === ToolName.ReplaceString), + hasMultiReplaceStringTool: !!availableTools?.find(tool => tool.name === ToolName.MultiReplaceString), hasInsertEditTool: !!availableTools?.find(tool => tool.name === ToolName.EditFile), hasApplyPatchTool: !!availableTools?.find(tool => tool.name === ToolName.ApplyPatch), hasReadFileTool: !!availableTools?.find(tool => tool.name === ToolName.ReadFile), From 77eec742dc551b3cabe8a010dae70d7ea4ad02e2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 22:31:07 +0000 Subject: [PATCH 11/20] add hasMultiReplaceString --- src/extension/prompts/node/agent/agentInstructions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index 9b31786ec..f3417a5f5 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -145,7 +145,7 @@ export class DefaultAgentPrompt extends PromptElement { {tools.hasReplaceStringTool ? <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- {hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
} + {tools.hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
} Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
@@ -320,7 +320,7 @@ export class AlternateGPTPrompt extends PromptElement { For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
- {hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This should be your first choice for bulk edits.
} + {tools.hasMultiReplaceStringTool && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This should be your first choice for bulk edits.
} Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
From 3be4cd54d90fabd595edd234b75c7c5f977582ed Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 11 Aug 2025 23:13:35 +0000 Subject: [PATCH 12/20] add references to MultiReplaceStringTool --- src/extension/prompts/node/agent/agentInstructions.tsx | 4 +++- src/extension/prompts/node/agent/agentPrompt.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index f3417a5f5..c105da8ca 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -312,6 +312,7 @@ export class AlternateGPTPrompt extends PromptElement { <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
+ Use the {ToolName.MultiReplaceString} tool to make edits simultaneously.
Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} @@ -551,7 +552,8 @@ export class SweBenchAgentPrompt extends PromptElement } {hasEditFileTool && Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to make edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
+ Use the {ToolName.ReplaceString} tool to make single edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
+ Use {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
Use the {ToolName.EditFile} tool to insert code into a file.
When editing files, group your changes by file.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index 675612034..cad9708eb 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -640,7 +640,7 @@ export function getEditingReminder(hasEditFileTool: boolean, hasReplaceStringToo } if (hasEditFileTool && hasReplaceStringTool) { if (useStrongReplaceStringHint) { - lines.push(<>You must always try making file edits using {ToolName.ReplaceString} tool. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); + lines.push(<>You must always try making file edits using {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tools. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); } else { lines.push(<>It is much faster to edit using the {ToolName.ReplaceString} tool. Prefer {ToolName.ReplaceString} for making edits and only fall back to {ToolName.EditFile} if it fails.); } From 4785b9f485a5f26c31c5c40098200e68fe731c4c Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:46:26 -0700 Subject: [PATCH 13/20] Delete test_implementation.js --- test_implementation.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test_implementation.js diff --git a/test_implementation.js b/test_implementation.js deleted file mode 100644 index e69de29bb..000000000 From e4c309e8714b59fade539fb315bac40156b61882 Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:46:44 -0700 Subject: [PATCH 14/20] Delete test_dummy_parameter.js --- test_dummy_parameter.js | 87 ----------------------------------------- 1 file changed, 87 deletions(-) delete mode 100644 test_dummy_parameter.js diff --git a/test_dummy_parameter.js b/test_dummy_parameter.js deleted file mode 100644 index e844eee24..000000000 --- a/test_dummy_parameter.js +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script to verify dummy_parameter is added to tools - */ - -// Mock the dependencies for testing -const mockDeepClone = (obj) => JSON.parse(JSON.stringify(obj)); - -// Simplified version of the rule we added -function addDummyParameterToTool(tool) { - const cloned = mockDeepClone(tool); - - // Apply the dummy parameter rule - if (cloned.function.parameters && cloned.function.parameters.type === 'object') { - const obj = cloned.function.parameters; - if (obj.properties && !obj.properties.dummy_parameter) { - obj.properties.dummy_parameter = { - type: 'string', - description: 'Dummy parameter for informational purposes only. Does not affect tool behavior.', - default: 'dummy_value' - }; - } - } - - return cloned; -} - -// Test the implementation -console.log('šŸ”§ Testing dummy_parameter addition...\n'); - -// Test 1: Tool with existing parameters -console.log('Test 1: Tool with existing parameters'); -const tool1 = { - type: 'function', - function: { - name: 'testTool', - description: 'A test tool', - parameters: { - type: 'object', - properties: { - input: { type: 'string' } - } - } - } -}; - -const result1 = addDummyParameterToTool(tool1); -console.log('āœ“ Dummy parameter added:', !!result1.function.parameters.properties.dummy_parameter); -console.log('āœ“ Original parameters preserved:', !!result1.function.parameters.properties.input); -console.log('āœ“ Dummy parameter description:', result1.function.parameters.properties.dummy_parameter?.description); -console.log(''); - -// Test 2: Tool with no parameters (will be skipped) -console.log('Test 2: Tool with no parameters'); -const tool2 = { - type: 'function', - function: { - name: 'noParamTool', - description: 'A tool with no parameters' - } -}; - -const result2 = addDummyParameterToTool(tool2); -console.log('āœ“ No parameters added (as expected):', !result2.function.parameters); -console.log(''); - -// Test 3: Tool with parameters but no properties -console.log('Test 3: Tool with parameters but no properties'); -const tool3 = { - type: 'function', - function: { - name: 'emptyParamTool', - description: 'A tool with empty parameters', - parameters: { - type: 'object' - } - } -}; - -const result3 = addDummyParameterToTool(tool3); -console.log('āœ“ No dummy parameter added (no properties object):', !result3.function.parameters.properties?.dummy_parameter); -console.log(''); - -console.log('šŸŽ‰ All tests completed! The rule works as expected.'); -console.log('\nšŸ“ Note: The actual implementation in toolSchemaNormalizer.ts will automatically'); -console.log(' apply this rule to all tools processed through normalizeToolSchema().'); From f316a7fcaef096b4035d93554957eb29027b975d Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:47:03 -0700 Subject: [PATCH 15/20] Delete test_required_dummy.js --- test_required_dummy.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test_required_dummy.js diff --git a/test_required_dummy.js b/test_required_dummy.js deleted file mode 100644 index e69de29bb..000000000 From f36bb024311e220802656e79d22b6cbe16e36988 Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:48:26 -0700 Subject: [PATCH 16/20] Delete verify_implementation.js --- verify_implementation.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 verify_implementation.js diff --git a/verify_implementation.js b/verify_implementation.js deleted file mode 100644 index e69de29bb..000000000 From bc4d78bca81111c43bf2e893c6de70e3e069e04a Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:50:04 -0700 Subject: [PATCH 17/20] Delete src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts --- .../tools/test/common/addNextToolPredictionParameter.spec.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts diff --git a/src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts b/src/extension/tools/test/common/addNextToolPredictionParameter.spec.ts deleted file mode 100644 index e69de29bb..000000000 From b27b29e74e1d532c51e4aae782905d31dc6983e8 Mon Sep 17 00:00:00 2001 From: yemohyleyemohyle <127880594+yemohyleyemohyle@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:52:15 -0700 Subject: [PATCH 18/20] Fix formatting of multi-replace string reminder --- src/extension/prompts/node/agent/agentPrompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index 596dae11a..0aa9b6db2 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -364,7 +364,7 @@ export class AgentUserMessage extends PromptElement { {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint))} {getExplanationReminder(this.props.endpoint.family, hasTodoTool)} - {hasMultiReplaceStringTool && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
} + {hasMultiReplaceStringTool && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
}
)} {query && {query + attachmentHint}} From 9d2c9809d5c96dd4a938a0063e6008dc83265818 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 13 Aug 2025 13:06:04 -0700 Subject: [PATCH 19/20] tidy up and exp --- IMPLEMENTATION_SUMMARY.md | 0 .../vscode-node/languageModelAccess.ts | 6 +- src/extension/intents/node/agentIntent.ts | 9 +- src/extension/intents/node/editCodeIntent2.ts | 8 +- .../intents/node/notebookEditorIntent.ts | 8 +- .../mcp/test/vscode-node/nuget.spec.ts | 4 +- .../prompts/node/agent/agentInstructions.tsx | 33 +++-- .../prompts/node/agent/agentPrompt.tsx | 15 +- .../__snapshots__/agentPrompt.spec.tsx.snap | 137 +++++++++++++++++- .../node/agent/test/agentPrompt.spec.tsx | 14 ++ .../prompts/node/panel/editCodePrompt2.tsx | 3 +- .../common/configurationService.ts | 2 +- .../endpoint/common/chatModelCapabilities.ts | 2 +- 13 files changed, 206 insertions(+), 35 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/extension/conversation/vscode-node/languageModelAccess.ts b/src/extension/conversation/vscode-node/languageModelAccess.ts index 9234fef2c..a8e81951f 100644 --- a/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -139,13 +139,17 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } const baseCount = await PromptRenderer.create(this._instantiationService, endpoint, LanguageModelAccessPrompt, { noSafety: false, messages: [] }).countTokens(); + let multiplierString = endpoint.multiplier !== undefined ? `${endpoint.multiplier}x` : undefined; + if (endpoint.model === AutoChatEndpoint.id) { + multiplierString = 'Variable'; + } const model: vscode.LanguageModelChatInformation = { id: endpoint.model, name: endpoint.model === AutoChatEndpoint.id ? 'Auto' : endpoint.name, family: endpoint.family, description: modelDescription, - cost: endpoint.multiplier !== undefined && endpoint.multiplier !== 0 ? `${endpoint.multiplier}x` : endpoint.multiplier === 0 ? localize('languageModel.costIncluded', 'Included') : undefined, + cost: multiplierString, category: modelCategory, version: endpoint.version, maxInputTokens: endpoint.modelMaxPromptTokens - baseCount - BaseTokensPerCompletion, diff --git a/src/extension/intents/node/agentIntent.ts b/src/extension/intents/node/agentIntent.ts index 95cc4c7fa..6fae53af5 100644 --- a/src/extension/intents/node/agentIntent.ts +++ b/src/extension/intents/node/agentIntent.ts @@ -61,15 +61,20 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const allowTools: Record = {}; allowTools[ToolName.EditFile] = true; - allowTools[ToolName.ReplaceString] = modelSupportsReplaceString(model) || !!(model.family.includes('gemini') && configurationService.getExperimentBasedConfig(ConfigKey.Internal.GeminiReplaceString, experimentationService)); + allowTools[ToolName.ReplaceString] = modelSupportsReplaceString(model); allowTools[ToolName.ApplyPatch] = await modelSupportsApplyPatch(model) && !!toolsService.getTool(ToolName.ApplyPatch); if (modelCanUseReplaceStringExclusively(model)) { allowTools[ToolName.ReplaceString] = true; - allowTools[ToolName.MultiReplaceString] = true; allowTools[ToolName.EditFile] = false; } + if (allowTools[ToolName.ReplaceString]) { + if (configurationService.getExperimentBasedConfig(ConfigKey.Internal.MultiReplaceString, experimentationService)) { + allowTools[ToolName.MultiReplaceString] = true; + } + } + allowTools[ToolName.RunTests] = await testService.hasAnyTests(); allowTools[ToolName.CoreRunTask] = !!(configurationService.getConfig(ConfigKey.AgentCanRunTasks) && tasksService.getTasks().length); diff --git a/src/extension/intents/node/editCodeIntent2.ts b/src/extension/intents/node/editCodeIntent2.ts index 7e304097a..f5884fc11 100644 --- a/src/extension/intents/node/editCodeIntent2.ts +++ b/src/extension/intents/node/editCodeIntent2.ts @@ -6,6 +6,7 @@ import type * as vscode from 'vscode'; import { ChatLocation } from '../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; +import { modelSupportsReplaceString } from '../../../platform/endpoint/common/chatModelCapabilities'; import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; @@ -40,15 +41,18 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const experimentalService = accessor.get(IExperimentationService); const model = await endpointProvider.getChatEndpoint(request); const lookForTools = new Set([ToolName.EditFile]); + const experimentationService = accessor.get(IExperimentationService); if (configurationService.getExperimentBasedConfig(ConfigKey.EditsCodeNewNotebookAgentEnabled, experimentalService) !== false && requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) { lookForTools.add(ToolName.CreateNewJupyterNotebook); } - if (model.family.startsWith('claude')) { + if (modelSupportsReplaceString(model)) { lookForTools.add(ToolName.ReplaceString); - lookForTools.add(ToolName.MultiReplaceString); + if (configurationService.getExperimentBasedConfig(ConfigKey.Internal.MultiReplaceString, experimentationService)) { + lookForTools.add(ToolName.MultiReplaceString); + } } lookForTools.add(ToolName.EditNotebook); if (requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) { diff --git a/src/extension/intents/node/notebookEditorIntent.ts b/src/extension/intents/node/notebookEditorIntent.ts index 95b08ade5..078d55a7a 100644 --- a/src/extension/intents/node/notebookEditorIntent.ts +++ b/src/extension/intents/node/notebookEditorIntent.ts @@ -33,6 +33,7 @@ import { IToolsService } from '../../tools/common/toolsService'; import { EditCodeIntent, EditCodeIntentOptions } from './editCodeIntent'; import { EditCode2IntentInvocation } from './editCodeIntent2'; import { getRequestedToolCallIterationLimit } from './toolCallingLoop'; +import { modelSupportsReplaceString } from '../../../platform/endpoint/common/chatModelCapabilities'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => instaService.invokeFunction(async accessor => { @@ -43,14 +44,17 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const experimentalService = accessor.get(IExperimentationService); const model = await endpointProvider.getChatEndpoint(request); const lookForTools = new Set([ToolName.EditFile]); + const experimentationService = accessor.get(IExperimentationService); if (configurationService.getExperimentBasedConfig(ConfigKey.EditsCodeNewNotebookAgentEnabled, experimentalService) !== false && requestHasNotebookRefs(request, notebookService, { checkPromptAsWell: true })) { lookForTools.add(ToolName.CreateNewJupyterNotebook); } - if (model.family.startsWith('claude')) { + if (modelSupportsReplaceString(model)) { lookForTools.add(ToolName.ReplaceString); - lookForTools.add(ToolName.MultiReplaceString); + if (configurationService.getExperimentBasedConfig(ConfigKey.Internal.MultiReplaceString, experimentationService)) { + lookForTools.add(ToolName.MultiReplaceString); + } } lookForTools.add(ToolName.EditNotebook); diff --git a/src/extension/mcp/test/vscode-node/nuget.spec.ts b/src/extension/mcp/test/vscode-node/nuget.spec.ts index c57d84e4c..3806df7c4 100644 --- a/src/extension/mcp/test/vscode-node/nuget.spec.ts +++ b/src/extension/mcp/test/vscode-node/nuget.spec.ts @@ -10,7 +10,7 @@ import { ITestingServicesAccessor, TestingServiceCollection } from '../../../../ import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { NuGetMcpSetup } from '../../vscode-node/nuget'; -describe('get nuget MCP server info', { timeout: 30_000 }, () => { +describe.skip('get nuget MCP server info', { timeout: 30_000 }, () => { let testingServiceCollection: TestingServiceCollection = createExtensionUnitTestingServices(); let accessor: ITestingServicesAccessor = testingServiceCollection.createTestingAccessor(); let logService: ILogService = accessor.get(ILogService); @@ -94,4 +94,4 @@ describe('get nuget MCP server info', { timeout: 30_000 }, () => { expect.fail(); } }); -}); \ No newline at end of file +}); diff --git a/src/extension/prompts/node/agent/agentInstructions.tsx b/src/extension/prompts/node/agent/agentInstructions.tsx index 643c110e1..2ff31090f 100644 --- a/src/extension/prompts/node/agent/agentInstructions.tsx +++ b/src/extension/prompts/node/agent/agentInstructions.tsx @@ -129,14 +129,15 @@ export class DefaultAgentPrompt extends PromptElement { {tools[ToolName.ReplaceString] ? <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- {tools[ToolName.MultiReplaceString] && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
} - Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique.
- Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
+ {tools[ToolName.MultiReplaceString] + ? <>Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique. Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
+ : <>Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
} + Use the {ToolName.EditFile} tool to insert code into a file ONLY if {tools[ToolName.MultiReplaceString] ? `${ToolName.MultiReplaceString}/` : ''}{ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.EditFile} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
+ NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
Use the {ToolName.EditFile} tool to edit files. When editing files, group your changes by file.
@@ -657,22 +658,22 @@ export class AlternateGPTPrompt extends PromptElement { {tools[ToolName.ReplaceString] ? <> Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
- Use the {ToolName.MultiReplaceString} tool to make edits simultaneously.
- Use the {ToolName.EditFile} tool to insert code into a file ONLY if {ToolName.ReplaceString} has failed.
+ {tools[ToolName.MultiReplaceString] + ? <>Use the {ToolName.ReplaceString} tool for single string replacements, paying attention to context to ensure your replacement is unique. Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling {ToolName.ReplaceString} multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places.
+ : <>Use the {ToolName.ReplaceString} tool to edit files, paying attention to context to ensure your replacement is unique. You can use this tool multiple times per file.
} + Use the {ToolName.EditFile} tool to insert code into a file ONLY if {tools[ToolName.MultiReplaceString] ? `${ToolName.MultiReplaceString}/` : ''}{ToolName.ReplaceString} has failed.
When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}, {ToolName.MultiReplaceString}, or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: + NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString}{tools[ToolName.MultiReplaceString] ? `, ${ToolName.MultiReplaceString},` : ''} or {ToolName.EditFile} tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
: <> Don't try to edit an existing file without reading it first, so you can make changes properly.
- {tools[ToolName.MultiReplaceString] && <>Prefer the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation. This should be your first choice for bulk edits.
} - Use the {ToolName.ReplaceString} tool to edit files. When editing files, group your changes by file.
+ Use the {ToolName.EditFile} tool to edit files. When editing files, group your changes by file.
{isGpt5 && <>Make the smallest set of edits needed and avoid reformatting or moving unrelated code. Preserve existing style and conventions, and keep imports, exports, and public APIs stable unless the task requires changes. Prefer completing all edits for a file within a single message when practical.
} NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
- NEVER print a codeblock that represents a change to a file, use {ToolName.ReplaceString} or {ToolName.MultiReplaceString} instead.
- For each file, give a short description of what needs to be changed, then use the {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
+ NEVER print a codeblock that represents a change to a file, use {ToolName.EditFile} instead.
+ For each file, give a short description of what needs to be changed, then use the {ToolName.EditFile} tool. You can use any tool multiple times in a response, and you can keep writing text after using a tool.
} The {ToolName.EditFile} tool is very smart and can understand how to apply your edits to the user's files, you just need to provide minimal hints.
@@ -861,6 +862,7 @@ export class SweBenchAgentPrompt extends PromptElement {!!tools[ToolName.ReplaceString] && {ToolName.ReplaceString} tool is a tool for editing files. For moving or renaming files, you should generally use the {ToolName.CoreRunInTerminal} with the 'mv' command instead. For larger edits, split it into small edits and call the edit tool multiple times to finish the whole edit carefully.
+ {tools[ToolName.MultiReplaceString] && <>Use the {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
} Before using {ToolName.ReplaceString} tool, you must use {ToolName.ReadFile} tool to understand the file's contents and context you want to edit
To make a file edit, provide the following:
1. filePath: The absolute path to the file to modify (must be absolute, not relative)
@@ -894,8 +896,7 @@ export class SweBenchAgentPrompt extends PromptElement
} {!!tools[ToolName.EditFile] && Before you edit an existing file, make sure you either already have it in the provided context, or read it with the {ToolName.ReadFile} tool, so that you can make proper changes.
- Use the {ToolName.ReplaceString} tool to make single edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
- Use {ToolName.MultiReplaceString} tool when you need to make multiple string replacements across one or more files in a single operation.
+ Use the {ToolName.ReplaceString} tool to make edits in the file in string replacement way, but only if you are sure that the string is unique enough to not cause any issues. You can use this tool multiple times per file.
Use the {ToolName.EditFile} tool to insert code into a file.
When editing files, group your changes by file.
NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user.
diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index f734e1e58..475867d08 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -355,10 +355,9 @@ export class AgentUserMessage extends PromptElement { {/* Critical reminders that are effective when repeated right next to the user message */} - {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint))} + {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint), hasMultiReplaceStringTool)} {getExplanationReminder(this.props.endpoint.family, hasTodoTool)} - {hasMultiReplaceStringTool && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
}
{query && {query + attachmentHint}} {this.props.enableCacheBreakpoints && } @@ -671,19 +670,23 @@ class AgentTasksInstructions extends PromptElement { } } -export function getEditingReminder(hasEditFileTool: boolean, hasReplaceStringTool: boolean, useStrongReplaceStringHint: boolean) { +export function getEditingReminder(hasEditFileTool: boolean, hasReplaceStringTool: boolean, useStrongReplaceStringHint: boolean, hasMultiStringReplace: boolean) { const lines = []; if (hasEditFileTool) { lines.push(<>When using the {ToolName.EditFile} tool, avoid repeating existing code, instead use a line comment with \`{EXISTING_CODE_MARKER}\` to represent regions of unchanged code.
); } if (hasReplaceStringTool) { - lines.push(<>When using the {ToolName.ReplaceString} tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
); + lines.push(<> + When using the {ToolName.ReplaceString} tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
+ {hasMultiStringReplace && <>For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using {ToolName.MultiReplaceString} tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience.
} + ); } if (hasEditFileTool && hasReplaceStringTool) { + const eitherOr = hasMultiStringReplace ? `${ToolName.ReplaceString} or ${ToolName.MultiReplaceString} tools` : `${ToolName.ReplaceString} tool`; if (useStrongReplaceStringHint) { - lines.push(<>You must always try making file edits using {ToolName.ReplaceString} or {ToolName.MultiReplaceString} tools. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); + lines.push(<>You must always try making file edits using the {eitherOr}. NEVER use {ToolName.EditFile} unless told to by the user or by a tool.); } else { - lines.push(<>It is much faster to edit using the {ToolName.ReplaceString} tool. Prefer {ToolName.ReplaceString} for making edits and only fall back to {ToolName.EditFile} if it fails.); + lines.push(<>It is much faster to edit using the {eitherOr}. Prefer the {eitherOr} for making edits and only fall back to {ToolName.EditFile} if it fails.); } } diff --git a/src/extension/prompts/node/agent/test/__snapshots__/agentPrompt.spec.tsx.snap b/src/extension/prompts/node/agent/test/__snapshots__/agentPrompt.spec.tsx.snap index fc554108c..aef155674 100644 --- a/src/extension/prompts/node/agent/test/__snapshots__/agentPrompt.spec.tsx.snap +++ b/src/extension/prompts/node/agent/test/__snapshots__/agentPrompt.spec.tsx.snap @@ -257,7 +257,142 @@ copilot_cache_control: { type: 'ephemeral' } When using the insert_edit_into_file tool, avoid repeating existing code, instead use a line comment with /\`...existing code.../\` to represent regions of unchanged code. When using the replace_string_in_file tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited. -It is much faster to edit using the replace_string_in_file tool. Prefer replace_string_in_file for making edits and only fall back to insert_edit_into_file if it fails. +It is much faster to edit using the replace_string_in_file tool. Prefer the replace_string_in_file tool for making edits and only fall back to insert_edit_into_file if it fails. + + +hello + + + +copilot_cache_control: { type: 'ephemeral' } +~~~ +" +`; + +exports[`AgentPrompt > all tools, replace_string/multi_replace_string/insert_edit 1`] = ` +"### System +~~~md +You are an expert AI programming assistant, working with a user in the VS Code editor. +When asked for your name, you must respond with "GitHub Copilot". +Follow the user's requirements carefully & to the letter. +Follow Microsoft content policies. +Avoid content that violates copyrights. +If you are asked to generate content that is harmful, hateful, racist, sexist, lewd, or violent, only respond with "Sorry, I can't assist with that." +Keep your answers short and impersonal. + +You are a highly sophisticated automated coding agent with expert-level knowledge across many different programming languages and frameworks. +The user will ask a question, or ask you to perform a task, and it may require lots of research to answer correctly. There is a selection of tools that let you perform actions or retrieve helpful context to answer the user's question. +You will be given some context and attachments along with the user prompt. You can use them if they are relevant to the task, and ignore them if not. Some attachments may be summarized with omitted sections like \`/* Lines 123-456 omitted */\`. You can use the read_file tool to read more context if needed. Never pass this omitted line marker to an edit tool. +If you can infer the project type (languages, frameworks, and libraries) from the user's query or the context that you have, make sure to keep them in mind when making changes. +If the user wants you to implement a feature and they have not specified the files to edit, first break down the user's request into smaller concepts and think about the kinds of files you need to grasp each concept. +If you aren't sure which tool is relevant, you can call multiple tools. You can call tools repeatedly to take actions or gather as much context as needed until you have completed the task fully. Don't give up unless you are sure the request cannot be fulfilled with the tools you have. It's YOUR RESPONSIBILITY to make sure that you have done all you can to collect necessary context. +When reading files, prefer reading large meaningful chunks rather than consecutive small sections to minimize tool calls and gain better context. +Don't make assumptions about the situation- gather context first, then perform the task or answer the question. +Think creatively and explore the workspace in order to make a complete fix. +Don't repeat yourself after a tool call, pick up where you left off. +NEVER print out a codeblock with file changes unless the user asked for it. Use the appropriate edit tool instead. +You don't need to read a file if it's already provided in context. + + +If the user is requesting a code sample, you can answer it directly without using any tools. +When using a tool, follow the JSON schema very carefully and make sure to include ALL required properties. +No need to ask permission before using a tool. +NEVER say the name of a tool to a user. For example, instead of saying that you'll use the run_in_terminal tool, say "I'll run the command in a terminal". +If you think running multiple tools can answer the user's question, prefer calling them in parallel whenever possible, but do not call semantic_search in parallel. +When using the read_file tool, prefer reading a large section over calling the read_file tool many times in sequence. You can also think of all the pieces you may be interested in and read them in parallel. Read large enough context to ensure you get what you need. +If semantic_search returns the full contents of the text files in the workspace, you have all the workspace context. +You can use the grep_search to get an overview of a file by searching for a string within that one file, instead of using read_file many times. +If you don't know exactly the string or filename pattern you're looking for, use semantic_search to do a semantic search across the workspace. +When invoking a tool that takes a file path, always use the absolute file path. If the file has a scheme like untitled: or vscode-userdata:, then use a URI with the scheme. +You don't currently have any tools available for running terminal commands. If the user asks you to run a terminal command, you can ask the user to enable terminal tools or print a codeblock with the suggested command. +Tools can be disabled by the user. You may see tools used previously in the conversation that are not currently available. Be careful to only use the tools that are currently available to you. + + +Before you edit an existing file, make sure you either already have it in the provided context, or read it with the read_file tool, so that you can make proper changes. +Use the replace_string_in_file tool for single string replacements, paying attention to context to ensure your replacement is unique. Prefer the multi_replace_string_in_file tool when you need to make multiple string replacements across one or more files in a single operation. This is significantly more efficient than calling replace_string_in_file multiple times and should be your first choice for: fixing similar patterns across files, applying consistent formatting changes, bulk refactoring operations, or any scenario where you need to make the same type of change in multiple places. +Use the insert_edit_into_file tool to insert code into a file ONLY if multi_replace_string_in_file/replace_string_in_file has failed. +When editing files, group your changes by file. +NEVER show the changes to the user, just call the tool, and the edits will be applied and shown to the user. +NEVER print a codeblock that represents a change to a file, use replace_string_in_file, multi_replace_string_in_file, or insert_edit_into_file instead. +For each file, give a short description of what needs to be changed, then use the replace_string_in_file, multi_replace_string_in_file, or insert_edit_into_file tools. You can use any tool multiple times in a response, and you can keep writing text after using a tool. +Follow best practices when editing files. If a popular external library exists to solve a problem, use it and properly install the package e.g. creating a "requirements.txt". +If you're building a webapp from scratch, give it a beautiful and modern UI. +After editing a file, any new errors in the file will be in the tool result. Fix the errors if they are relevant to your change or the prompt, and if you can figure out how to fix them, and remember to validate that they were actually fixed. Do not loop more than 3 times attempting to fix errors in the same file. If the third try fails, you should stop and ask the user what to do next. +The insert_edit_into_file tool is very smart and can understand how to apply your edits to the user's files, you just need to provide minimal hints. +When you use the insert_edit_into_file tool, avoid repeating existing code, instead use comments to represent regions of unchanged code. The tool prefers that you are as concise as possible. For example: +// ...existing code... +changed code +// ...existing code... +changed code +// ...existing code... + +Here is an example of how you should format an edit to an existing Person class: +class Person { + // ...existing code... + age: number; + // ...existing code... + getAge() { + return this.age; + } +} + + +To edit notebook files in the workspace, you can use the edit_notebook_file tool. + +Never use the insert_edit_into_file tool and never execute Jupyter related commands in the Terminal to edit notebook files, such as \`jupyter notebook\`, \`jupyter lab\`, \`install jupyter\` or the like. Use the edit_notebook_file tool instead. +Use the run_notebook_cell tool instead of executing Jupyter related commands in the Terminal, such as \`jupyter notebook\`, \`jupyter lab\`, \`install jupyter\` or the like. +Use the copilot_getNotebookSummary tool to get the summary of the notebook (this includes the list or all cells along with the Cell Id, Cell type and Cell Language, execution details and mime types of the outputs, if any). +Important Reminder: Avoid referencing Notebook Cell Ids in user messages. Use cell number instead. +Important Reminder: Markdown cells cannot be executed + + +Use proper Markdown formatting in your answers. When referring to a filename or symbol in the user's workspace, wrap it in backticks. + +The class \`Person\` is in \`src/models/person.ts\`. + + + + + +This is a test custom instruction file + + + +copilot_cache_control: { type: 'ephemeral' } +~~~ + + +### User +~~~md + +The user's current OS is: Linux +The user's default shell is: "zsh". When you generate terminal commands, please generate them correctly for this shell. + + +I am working in a workspace with the following folders: +- /workspace +I am working in a workspace that has the following structure: +\`\`\` + +\`\`\` +This is the state of the context at this point in the conversation. The view of the workspace structure may be truncated. You can use tools to collect more context if needed. + + + +copilot_cache_control: { type: 'ephemeral' } +~~~ + + +### User +~~~md + +(Date removed from snapshot) + + +When using the insert_edit_into_file tool, avoid repeating existing code, instead use a line comment with /\`...existing code.../\` to represent regions of unchanged code. +When using the replace_string_in_file tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited. +For maximum efficiency, whenever you plan to perform multiple independent edit operations, invoke them simultaneously using multi_replace_string_in_file tool rather than sequentially. This will greatly improve user's cost and time efficiency leading to a better user experience. +It is much faster to edit using the replace_string_in_file or multi_replace_string_in_file tools. Prefer the replace_string_in_file or multi_replace_string_in_file tools for making edits and only fall back to insert_edit_into_file if it fails. hello diff --git a/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx b/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx index 4f2fc1335..56e952446 100644 --- a/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx +++ b/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx @@ -133,6 +133,20 @@ suite('AgentPrompt', () => { }); test('all tools, replace_string/insert_edit', async () => { + const toolsService = accessor.get(IToolsService); + expect(await agentPromptToString(accessor, { + chatVariables: new ChatVariablesCollection(), + history: [], + query: 'hello', + tools: { + availableTools: toolsService.tools.filter(tool => tool.name !== ToolName.ApplyPatch && tool.name !== ToolName.MultiReplaceString), + toolInvocationToken: null as never, + toolReferences: [], + } + }, undefined)).toMatchSnapshot(); + }); + + test('all tools, replace_string/multi_replace_string/insert_edit', async () => { const toolsService = accessor.get(IToolsService); expect(await agentPromptToString(accessor, { chatVariables: new ChatVariablesCollection(), diff --git a/src/extension/prompts/node/panel/editCodePrompt2.tsx b/src/extension/prompts/node/panel/editCodePrompt2.tsx index fdb92ab40..47e73c530 100644 --- a/src/extension/prompts/node/panel/editCodePrompt2.tsx +++ b/src/extension/prompts/node/panel/editCodePrompt2.tsx @@ -138,6 +138,7 @@ class EditCode2UserMessage extends PromptElement { const useProjectLabels = this._configurationService.getExperimentBasedConfig(ConfigKey.Internal.ProjectLabelsChat, this.experimentationService); const hasReplaceStringTool = !!this.props.promptContext.tools?.availableTools.find(tool => tool.name === ToolName.ReplaceString); const hasEditFileTool = !!this.props.promptContext.tools?.availableTools.find(tool => tool.name === ToolName.EditFile); + const hasMultiReplaceStringTool = !!this.props.promptContext.tools?.availableTools.find(tool => tool.name === ToolName.MultiReplaceString); return ( <> @@ -148,7 +149,7 @@ class EditCode2UserMessage extends PromptElement { - {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint))} + {getEditingReminder(hasEditFileTool, hasReplaceStringTool, modelNeedsStrongReplaceStringHint(this.props.endpoint), hasMultiReplaceStringTool)} diff --git a/src/platform/configuration/common/configurationService.ts b/src/platform/configuration/common/configurationService.ts index 44e39ae13..0bfb44e74 100644 --- a/src/platform/configuration/common/configurationService.ts +++ b/src/platform/configuration/common/configurationService.ts @@ -713,7 +713,7 @@ export namespace ConfigKey { export const OmitBaseAgentInstructions = defineSetting('chat.advanced.omitBaseAgentInstructions', false, INTERNAL); export const PromptFileContext = defineExpSetting('chat.advanced.promptFileContextProvider.enabled', true); - export const GeminiReplaceString = defineExpSetting('chat.advanced.geminiReplaceString.enabled', false, INTERNAL, { experimentName: 'copilotchat.geminiReplaceString' }); + export const MultiReplaceString = defineExpSetting('chat.advanced.multiReplaceString.enabled', false, INTERNAL); } export const AgentThinkingTool = defineSetting('chat.agent.thinkingTool', false); diff --git a/src/platform/endpoint/common/chatModelCapabilities.ts b/src/platform/endpoint/common/chatModelCapabilities.ts index 3dbaf4423..6846eaa0d 100644 --- a/src/platform/endpoint/common/chatModelCapabilities.ts +++ b/src/platform/endpoint/common/chatModelCapabilities.ts @@ -53,7 +53,7 @@ export function modelPrefersJsonNotebookRepresentation(model: LanguageModelChat * Model supports replace_string_in_file as an edit tool. */ export function modelSupportsReplaceString(model: LanguageModelChat | IChatEndpoint): boolean { - return model.family.startsWith('claude') || model.family.startsWith('Anthropic'); + return model.family.startsWith('claude') || model.family.startsWith('Anthropic') || model.family.includes('gemini'); } /** From 9c8e3abd65cfe461d053acd0af3cb40696c61985 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 13 Aug 2025 15:43:51 -0700 Subject: [PATCH 20/20] eng: refactor multi and single edit tools --- .../prompts/node/codeMapper/codeMapper.ts | 20 +- .../tools/node/abstractReplaceStringTool.tsx | 399 ++++++++++++++++++ .../tools/node/editFileToolUtils.tsx | 19 +- .../tools/node/multiReplaceStringTool.tsx | 186 +++----- .../tools/node/replaceStringTool.tsx | 352 +-------------- .../tools/node/test/editFileToolUtils.spec.ts | 6 +- 6 files changed, 507 insertions(+), 475 deletions(-) create mode 100644 src/extension/tools/node/abstractReplaceStringTool.tsx diff --git a/src/extension/prompts/node/codeMapper/codeMapper.ts b/src/extension/prompts/node/codeMapper/codeMapper.ts index 30231d01b..79d4b970a 100644 --- a/src/extension/prompts/node/codeMapper/codeMapper.ts +++ b/src/extension/prompts/node/codeMapper/codeMapper.ts @@ -43,7 +43,7 @@ import { isEqual } from '../../../../util/vs/base/common/resources'; import { URI } from '../../../../util/vs/base/common/uri'; import { generateUuid } from '../../../../util/vs/base/common/uuid'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; -import { Position, Range, TextEdit } from '../../../../vscodeTypes'; +import { NotebookEdit, Position, Range, TextEdit } from '../../../../vscodeTypes'; import { OutcomeAnnotation, OutcomeAnnotationLabel } from '../../../inlineChat/node/promptCraftingTypes'; import { Lines, LinesEdit } from '../../../prompt/node/editGeneration'; import { LineOfText, PartialAsyncTextReader } from '../../../prompt/node/streamingEdits'; @@ -57,6 +57,20 @@ import { findEdit, getCodeBlock, iterateSectionsForResponse, Marker, Patch, Sect export type ICodeMapperDocument = TextDocumentSnapshot | NotebookDocumentSnapshot; export async function processFullRewriteNotebook(document: NotebookDocument, inputStream: string | AsyncIterable, outputStream: MappedEditsResponseStream, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): Promise { + for await (const edit of processFullRewriteNotebookEdits(document, inputStream, alternativeNotebookEditGenerator, telemetryOptions, token)) { + if (Array.isArray(edit)) { + outputStream.textEdit(edit[0], edit[1]); + } else { + outputStream.notebookEdit(document.uri, edit); // changed this.outputStream to outputStream + } + } + + return undefined; +} + +export type CellOrNotebookEdit = NotebookEdit | [Uri, TextEdit[]]; + +export async function* processFullRewriteNotebookEdits(document: NotebookDocument, inputStream: string | AsyncIterable, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): AsyncIterable { // emit start of notebook const cellMap = new ResourceMap(); for await (const edit of alternativeNotebookEditGenerator.generateNotebookEdits(document, inputStream, telemetryOptions, token)) { @@ -70,10 +84,10 @@ export async function processFullRewriteNotebook(document: NotebookDocument, inp continue; } } - outputStream.textEdit(cellUri, edit[1]); + yield [cellUri, edit[1]]; } } else { - outputStream.notebookEdit(document.uri, edit); // changed this.outputStream to outputStream + yield edit; } } diff --git a/src/extension/tools/node/abstractReplaceStringTool.tsx b/src/extension/tools/node/abstractReplaceStringTool.tsx new file mode 100644 index 000000000..4b04eeabd --- /dev/null +++ b/src/extension/tools/node/abstractReplaceStringTool.tsx @@ -0,0 +1,399 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { CHAT_MODEL } from '../../../platform/configuration/common/configurationService'; +import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService'; +import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot'; +import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot'; +import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; +import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; +import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService'; +import { IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent'; +import { IAlternativeNotebookContentEditGenerator, NotebookEditGenerationTelemtryOptions, NotebookEditGenrationSource } from '../../../platform/notebook/common/alternativeContentEditGenerator'; +import { INotebookService } from '../../../platform/notebook/common/notebookService'; +import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; +import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; +import { ITelemetryService, multiplexProperties } from '../../../platform/telemetry/common/telemetry'; +import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; +import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl'; +import { removeLeadingFilepathComment } from '../../../util/common/markdown'; +import { timeout } from '../../../util/vs/base/common/async'; +import { Iterable } from '../../../util/vs/base/common/iterator'; +import { ResourceMap } from '../../../util/vs/base/common/map'; +import { URI } from '../../../util/vs/base/common/uri'; +import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; +import { ChatResponseTextEditPart, EndOfLine, LanguageModelPromptTsxPart, LanguageModelToolResult } from '../../../vscodeTypes'; +import { IBuildPromptContext } from '../../prompt/common/intents'; +import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'; +import { CellOrNotebookEdit, processFullRewriteNotebookEdits } from '../../prompts/node/codeMapper/codeMapper'; +import { ToolName } from '../common/toolNames'; +import { ICopilotTool } from '../common/toolsRegistry'; +import { IToolsService } from '../common/toolsService'; +import { ActionType } from './applyPatch/parser'; +import { CorrectedEditResult, healReplaceStringParams } from './editFileHealing'; +import { EditFileResult, IEditedFile } from './editFileToolResult'; +import { EditError, NoChangeError, NoMatchError, applyEdit } from './editFileToolUtils'; +import { sendEditNotebookTelemetry } from './editNotebookTool'; +import { assertFileOkForTool, resolveToolInputPath } from './toolUtils'; + +export interface IAbstractReplaceStringInput { + filePath: string; + oldString: string; + newString: string; +} + +export interface IPrepareEdit { + document: NotebookDocumentSnapshot | TextDocumentSnapshot; + uri: URI; + didHeal: boolean; + input: IAbstractReplaceStringInput; + generatedEdit: { success: true; textEdits: vscode.TextEdit[]; notebookEdits?: CellOrNotebookEdit[] } | { success: false; errorMessage: string }; +} + + +export abstract class AbstractReplaceStringTool implements ICopilotTool { + private _promptContext: IBuildPromptContext | undefined; + + constructor( + @IPromptPathRepresentationService protected readonly promptPathRepresentationService: IPromptPathRepresentationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IWorkspaceService protected readonly workspaceService: IWorkspaceService, + @IToolsService protected readonly toolsService: IToolsService, + @INotebookService protected readonly notebookService: INotebookService, + @IFileSystemService protected readonly fileSystemService: IFileSystemService, + @IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService, + @IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, + @IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService, + @ILanguageDiagnosticsService private readonly languageDiagnosticsService: ILanguageDiagnosticsService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IEndpointProvider private readonly endpointProvider: IEndpointProvider, + @IExperimentationService private readonly experimentationService: IExperimentationService + ) { } + + public abstract invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise; + + protected abstract toolName(): ToolName; + + protected async prepareEditsForFile(options: vscode.LanguageModelToolInvocationOptions, input: IAbstractReplaceStringInput, token: vscode.CancellationToken): Promise { + const uri = resolveToolInputPath(input.filePath, this.promptPathRepresentationService); + try { + await this.instantiationService.invokeFunction(accessor => assertFileOkForTool(accessor, uri)); + } catch (error) { + this.sendReplaceTelemetry('invalidFile', options, input, undefined, undefined, undefined); + throw error; + } + + // Validate parameters + if (!input.filePath || input.oldString === undefined || input.newString === undefined || !this._promptContext) { + this.sendReplaceTelemetry('invalidStrings', options, input, undefined, undefined, undefined); + throw new Error('Invalid input'); + } + + const isNotebook = this.notebookService.hasSupportedNotebooks(uri); + const document = isNotebook ? + await this.workspaceService.openNotebookDocumentAndSnapshot(uri, this.alternativeNotebookContent.getFormat(this._promptContext?.request?.model)) : + await this.workspaceService.openTextDocumentAndSnapshot(uri); + + const didHealRef = { didHeal: false }; + try { + if (input.oldString === input.newString) { + throw new NoChangeError('Input and output are identical', input.filePath); + } + + const { updatedFile, edits } = await this.generateEdit(uri, document, options, input, didHealRef, token); + let notebookEdits: (vscode.NotebookEdit | [URI, vscode.TextEdit[]])[] | undefined; + if (document instanceof NotebookDocumentSnapshot) { + const telemetryOptions: NotebookEditGenerationTelemtryOptions = { + model: options.model ? this.endpointProvider.getChatEndpoint(options.model).then(m => m.name) : undefined, + requestId: this._promptContext.requestId, + source: NotebookEditGenrationSource.stringReplace, + }; + + notebookEdits = await Iterable.asyncToArray(processFullRewriteNotebookEdits(document.document, updatedFile, this.alternativeNotebookEditGenerator, telemetryOptions, token)); + sendEditNotebookTelemetry(this.telemetryService, this.endpointProvider, 'stringReplace', document.uri, this._promptContext.requestId, options.model ?? this._promptContext.request?.model); + } + + void this.sendReplaceTelemetry('success', options, input, document.getText(), isNotebook, didHealRef.didHeal); + return { document, uri, input, didHeal: didHealRef.didHeal, generatedEdit: { success: true, textEdits: edits, notebookEdits } }; + } catch (error) { + // Enhanced error message with more helpful details + let errorMessage = 'String replacement failed: '; + let outcome: string; + + if (error instanceof NoMatchError) { + outcome = input.oldString.match(/Lines \d+-\d+ omitted/) ? + 'oldStringHasOmittedLines' : + input.oldString.includes('{…}') ? + 'oldStringHasSummarizationMarker' : + input.oldString.includes('/*...*/') ? + 'oldStringHasSummarizationMarkerSemanticSearch' : + error.kindForTelemetry; + errorMessage += `${error.message}`; + } else if (error instanceof EditError) { + outcome = error.kindForTelemetry; + errorMessage += error.message; + } else { + outcome = 'other'; + errorMessage += `${error.message}`; + } + + void this.sendReplaceTelemetry(outcome, options, input, document.getText(), isNotebook, didHealRef.didHeal); + + return { document, uri, input, didHeal: didHealRef.didHeal, generatedEdit: { success: false, errorMessage } }; + } + } + + protected async applyAllEdits(options: vscode.LanguageModelToolInvocationOptions, edits: IPrepareEdit[], token: vscode.CancellationToken) { + if (!this._promptContext?.stream) { + throw new Error('no prompt context found'); + } + + const fileResults: IEditedFile[] = []; + const existingDiagnosticMap = new ResourceMap(); + + for (const { document, uri, generatedEdit } of edits) { + if (!existingDiagnosticMap.has(document.uri)) { + existingDiagnosticMap.set(document.uri, this.languageDiagnosticsService.getDiagnostics(document.uri)); + } + const existingDiagnostics = existingDiagnosticMap.get(document.uri)!; + const isNotebook = this.notebookService.hasSupportedNotebooks(uri); + + if (!generatedEdit.success) { + fileResults.push({ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics, error: generatedEdit.errorMessage }); + continue; + } + + let editSurvivalTracker: IEditSurvivalTrackingSession | undefined; + let responseStream = this._promptContext.stream; + if (document && document instanceof TextDocumentSnapshot) { // Only for existing text documents + const tracker = editSurvivalTracker = this._editSurvivalTrackerService.initialize(document.document); + responseStream = ChatResponseStreamImpl.spy(this._promptContext.stream, (part) => { + if (part instanceof ChatResponseTextEditPart) { + tracker.collectAIEdits(part.edits); + } + }); + } + + this._promptContext.stream.markdown('\n```\n'); + this._promptContext.stream.codeblockUri(uri, true); + + if (generatedEdit.notebookEdits) { + this._promptContext.stream.notebookEdit(document.uri, []); + for (const edit of generatedEdit.notebookEdits) { + if (edit instanceof Array) { + this._promptContext.stream.textEdit(edit[0], edit[1]); + } else { + this._promptContext.stream.notebookEdit(document.uri, edit); + } + } + this._promptContext.stream.notebookEdit(document.uri, true); + } else { + for (const edit of generatedEdit.textEdits) { + responseStream.textEdit(uri, edit); + } + responseStream.textEdit(uri, true); + + timeout(2000).then(() => { + // The tool can't wait for edits to be applied, so just wait before starting the survival tracker. + // TODO@roblourens see if this improves the survival metric, find a better fix. + editSurvivalTracker?.startReporter(res => { + /* __GDPR__ + "codeMapper.trackEditSurvival" : { + "owner": "aeschli", + "comment": "Tracks how much percent of the AI edits survived after 5 minutes of accepting", + "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, + "requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" }, + "mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The code mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" }, + "survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the AI edit is still present in the document." }, + "survivalRateNoRevert": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the ranges the AI touched ended up being reverted." }, + "didBranchChange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Indicates if the branch changed in the meantime. If the branch changed (value is 1), this event should probably be ignored." }, + "timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." } + } + */ + res.telemetryService.sendMSFTTelemetryEvent('codeMapper.trackEditSurvival', { requestId: this._promptContext?.requestId, requestSource: 'agent', mapper: 'stringReplaceTool' }, { + survivalRateFourGram: res.fourGram, + survivalRateNoRevert: res.noRevert, + timeDelayMs: res.timeDelayMs, + didBranchChange: res.didBranchChange ? 1 : 0, + }); + }); + }); + + fileResults.push({ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics }); + } + + this._promptContext.stream.markdown('\n```\n'); + } + + return new LanguageModelToolResult([ + new LanguageModelPromptTsxPart( + await renderPromptElementJSON( + this.instantiationService, + EditFileResult, + { files: fileResults, diagnosticsTimeout: 2000, toolName: this.toolName(), requestId: options.chatRequestId, model: options.model }, + // If we are not called with tokenization options, have _some_ fake tokenizer + // otherwise we end up returning the entire document + options.tokenizationOptions ?? { + tokenBudget: 5000, + countTokens: (t) => Promise.resolve(t.length * 3 / 4) + }, + token, + ), + ) + ]); + } + + private async generateEdit(uri: URI, document: TextDocumentSnapshot | NotebookDocumentSnapshot, options: vscode.LanguageModelToolInvocationOptions, input: IAbstractReplaceStringInput, didHealRef: { didHeal: boolean }, token: vscode.CancellationToken) { + const filePath = this.promptPathRepresentationService.getFilePath(document.uri); + const eol = document instanceof TextDocumentSnapshot && document.eol === EndOfLine.CRLF ? '\r\n' : '\n'; + const oldString = removeLeadingFilepathComment(input.oldString, document.languageId, filePath).replace(/\r?\n/g, eol); + const newString = removeLeadingFilepathComment(input.newString, document.languageId, filePath).replace(/\r?\n/g, eol); + + // Apply the edit using the improved applyEdit function that uses VS Code APIs + let updatedFile: string; + let edits: vscode.TextEdit[] = []; + try { + const result = await applyEdit( + uri, + oldString, + newString, + this.workspaceService, + this.notebookService, + this.alternativeNotebookContent, + this._promptContext?.request?.model + ); + updatedFile = result.updatedFile; + edits = result.edits; + } catch (e) { + if (!(e instanceof NoMatchError)) { + throw e; + } + + if (this.experimentationService.getTreatmentVariable('vscode', 'copilotchat.disableReplaceStringHealing') === true) { + throw e; // failsafe for next release. + } + + didHealRef.didHeal = true; + + let healed: CorrectedEditResult; + try { + healed = await healReplaceStringParams( + options.model, + document.getText(), + { + explanation: options.input.explanation, + filePath: filePath, + oldString, + newString, + }, + eol, + await this.endpointProvider.getChatEndpoint(CHAT_MODEL.GPT4OMINI), + token + ); + if (healed.params.oldString === healed.params.newString) { + throw new NoChangeError('change was identical after healing', document.uri.fsPath); + } + } catch (e2) { + this.sendHealingTelemetry(options, String(e2), undefined); + throw e; // original error + } + + try { + const result = await applyEdit( + uri, + healed.params.oldString, + healed.params.newString, + this.workspaceService, + this.notebookService, + this.alternativeNotebookContent, + this._promptContext?.request?.model + ); + updatedFile = result.updatedFile; + edits = result.edits; + } catch (e2) { + this.sendHealingTelemetry(options, undefined, String(e2)); + throw e; // original error + } + } + + return { edits, updatedFile }; + } + + private async sendReplaceTelemetry(outcome: string, options: vscode.LanguageModelToolInvocationOptions, input: IAbstractReplaceStringInput, file: string | undefined, isNotebookDocument: boolean | undefined, didHeal: boolean | undefined) { + const model = await this.modelForTelemetry(options); + const isNotebook = isNotebookDocument ? 1 : (isNotebookDocument === false ? 0 : -1); + const isMulti = this.toolName() === ToolName.MultiReplaceString ? 1 : 0; + /* __GDPR__ + "replaceStringToolInvoked" : { + "owner": "roblourens", + "comment": "The replace_string tool was invoked", + "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, + "interactionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current interaction." }, + "outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the invocation was successful, or a failure reason" }, + "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }, + "isNotebook": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." }, + "didHeal": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." }, + "isMulti": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a multi-replace operation, 1 = yes, 0 = no." } + } + */ + this.telemetryService.sendMSFTTelemetryEvent('replaceStringToolInvoked', + { + requestId: options.chatRequestId, + interactionId: options.chatRequestId, + outcome, + model + }, { isNotebook, didHeal: didHeal === undefined ? -1 : (didHeal ? 1 : 0), isMulti } + ); + + this.telemetryService.sendEnhancedGHTelemetryEvent('replaceStringTool', multiplexProperties({ + headerRequestId: options.chatRequestId, + baseModel: model, + messageText: file, + completionTextJson: JSON.stringify(input), + postProcessingOutcome: outcome, + }), { isNotebook }); + } + + private async sendHealingTelemetry(options: vscode.LanguageModelToolInvocationOptions, healError: string | undefined, applicationError: string | undefined) { + /* __GDPR__ + "replaceStringHealingStat" : { + "owner": "roblourens", + "comment": "The replace_string tool was invoked", + "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, + "interactionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current interaction." }, + "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }, + "outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the invocation was successful, or a failure reason" }, + "healError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Any error that happened during healing" }, + "applicationError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Any error that happened after application" }, + "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." } + } + */ + this.telemetryService.sendMSFTTelemetryEvent('replaceStringHealingStat', + { + requestId: options.chatRequestId, + interactionId: options.chatRequestId, + model: await this.modelForTelemetry(options), + healError, + applicationError, + }, { success: healError === undefined && applicationError === undefined ? 1 : 0 } + ); + } + + private async modelForTelemetry(options: vscode.LanguageModelToolInvocationOptions) { + return options.model && (await this.endpointProvider.getChatEndpoint(options.model)).model; + } + + async resolveInput(input: T, promptContext: IBuildPromptContext): Promise { + this._promptContext = promptContext; // TODO@joyceerhl @roblourens HACK: Avoid types in the input being serialized and not deserialized when they go through invokeTool + return input; + } + + prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions, token: vscode.CancellationToken): vscode.ProviderResult { + return { + presentation: 'hidden' + }; + } +} diff --git a/src/extension/tools/node/editFileToolUtils.tsx b/src/extension/tools/node/editFileToolUtils.tsx index fb89c5774..6417221a3 100644 --- a/src/extension/tools/node/editFileToolUtils.tsx +++ b/src/extension/tools/node/editFileToolUtils.tsx @@ -11,7 +11,7 @@ import { INotebookService } from '../../../platform/notebook/common/notebookServ import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { URI } from '../../../util/vs/base/common/uri'; import { Position as EditorPosition } from '../../../util/vs/editor/common/core/position'; -import { EndOfLine, Position, Range, WorkspaceEdit } from '../../../vscodeTypes'; +import { EndOfLine, Position, Range, TextEdit } from '../../../vscodeTypes'; // Simplified Hunk type for the patch interface Hunk { @@ -392,15 +392,15 @@ export async function applyEdit( uri: URI, old_string: string, new_string: string, - workspaceEdit: WorkspaceEdit, workspaceService: IWorkspaceService, notebookService: INotebookService, alternativeNotebookContent: IAlternativeNotebookContentService, languageModel: LanguageModelChat | undefined -): Promise<{ patch: Hunk[]; updatedFile: string }> { +): Promise<{ patch: Hunk[]; updatedFile: string; edits: TextEdit[] }> { let originalFile: string; let updatedFile: string; + const edits: TextEdit[] = []; const filePath = uri.toString(); try { @@ -421,7 +421,7 @@ export async function applyEdit( } // Create new file case updatedFile = new_string; - workspaceEdit.insert(uri, new Position(0, 0), new_string); + edits.push(TextEdit.insert(new Position(0, 0), new_string)); } else { // Edit existing file case if (new_string === '') { @@ -435,7 +435,7 @@ export async function applyEdit( if (result.editPosition.length) { const [start, end] = result.editPosition[0]; const range = new Range(document.positionAt(start), document.positionAt(end)); - workspaceEdit.delete(uri, range); + edits.push(TextEdit.delete(range)); } } else { const suggestion = result?.suggestion || 'The string to replace must match exactly.'; @@ -456,7 +456,7 @@ export async function applyEdit( if (result.editPosition.length) { const [start, end] = result.editPosition[0]; const range = new Range(document.positionAt(start), document.positionAt(end)); - workspaceEdit.delete(uri, range); + edits.push(TextEdit.delete(range)); } } } else { @@ -481,7 +481,7 @@ export async function applyEdit( if (result.editPosition.length) { const [start, end] = result.editPosition[0]; const range = new Range(document.positionAt(start), document.positionAt(end)); - workspaceEdit.replace(uri, range, new_string); + edits.push(TextEdit.replace(range, new_string)); } // If we used similarity matching, add a warning @@ -506,7 +506,7 @@ export async function applyEdit( newStr: updatedFile, }); - return { patch, updatedFile }; + return { patch, updatedFile, edits }; } catch (error) { // If the file doesn't exist and we're creating a new file with empty oldString if (old_string === '' && error.code === 'ENOENT') { @@ -519,7 +519,8 @@ export async function applyEdit( newStr: updatedFile, }); - return { patch, updatedFile }; + edits.push(TextEdit.insert(new Position(0, 0), new_string)); + return { patch, updatedFile, edits }; } if (error instanceof EditError) { diff --git a/src/extension/tools/node/multiReplaceStringTool.tsx b/src/extension/tools/node/multiReplaceStringTool.tsx index fe5ef835f..45c56847f 100644 --- a/src/extension/tools/node/multiReplaceStringTool.tsx +++ b/src/extension/tools/node/multiReplaceStringTool.tsx @@ -4,145 +4,99 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes'; -import { IBuildPromptContext } from '../../prompt/common/intents'; +import { ResourceMap } from '../../../util/vs/base/common/map'; +import { URI } from '../../../util/vs/base/common/uri'; +import { CellOrNotebookEdit } from '../../prompts/node/codeMapper/codeMapper'; import { ToolName } from '../common/toolNames'; -import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; -import { IToolsService } from '../common/toolsService'; -import { IReplaceStringToolParams, ReplaceStringTool } from './replaceStringTool'; +import { ToolRegistry } from '../common/toolsRegistry'; +import { AbstractReplaceStringTool } from './abstractReplaceStringTool'; +import { IReplaceStringToolParams } from './replaceStringTool'; export interface IMultiReplaceStringToolParams { explanation: string; replacements: IReplaceStringToolParams[]; } -export interface IMultiReplaceResult { - totalReplacements: number; - successfulReplacements: number; - failedReplacements: number; - results: Array<{ - operation: IReplaceStringToolParams; - success: boolean; - error?: string; - }>; -} - -export class MultiReplaceStringTool implements ICopilotTool { +export class MultiReplaceStringTool extends AbstractReplaceStringTool { public static toolName = ToolName.MultiReplaceString; - private _promptContext: IBuildPromptContext | undefined; + async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken) { + if (!options.input.replacements || !Array.isArray(options.input.replacements)) { + throw new Error('Invalid input, no replacements array'); + } - constructor( - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IToolsService protected readonly toolsService: IToolsService - ) { } + const prepared = await Promise.all(options.input.replacements.map(r => this.prepareEditsForFile(options, r, token))); - // Simplified version that uses a more direct approach - async invoke(options: any, token: any) { - // Cast the options to the correct type to work around TypeScript issues - const typedOptions = options as vscode.LanguageModelToolInvocationOptions & { input: IMultiReplaceStringToolParams }; + for (let i = 0; i < prepared.length; i++) { + const e1 = prepared[i]; + if (!e1.generatedEdit.success) { + continue; + } - // Validate input - if (!typedOptions.input.replacements || !Array.isArray(typedOptions.input.replacements) || typedOptions.input.replacements.length === 0) { - throw new Error('Invalid input: replacements array is required and must contain at least one replacement operation'); - } + for (let k = i + 1; k < prepared.length; k++) { + const e2 = prepared[k]; + // Merge successful edits of the same type and URI so that edits come in + // a single correct batch and positions aren't later clobbered. + if (!e2.generatedEdit.success || e2.uri.toString() !== e1.uri.toString() || (!!e2.generatedEdit.notebookEdits !== !!e1.generatedEdit.notebookEdits)) { + continue; + } - if (!this._promptContext?.stream) { - throw new Error('Invalid context: stream is required'); + prepared.splice(k, 1); + k--; + + if (e2.generatedEdit.notebookEdits) { + e1.generatedEdit.notebookEdits = mergeNotebookAndTextEdits(e1.generatedEdit.notebookEdits!, e2.generatedEdit.notebookEdits); + } else { + e1.generatedEdit.textEdits = e1.generatedEdit.textEdits.concat(e2.generatedEdit.textEdits); + e1.generatedEdit.textEdits.sort(textEditSorter); + } + } } - const results: IMultiReplaceResult = { - totalReplacements: typedOptions.input.replacements.length, - successfulReplacements: 0, - failedReplacements: 0, - results: [] - }; + return this.applyAllEdits(options, prepared, token); + } - // Get the ReplaceStringTool instance - const replaceStringTool = this.instantiationService.createInstance(ReplaceStringTool); + protected override toolName(): ToolName { + return MultiReplaceStringTool.toolName; + } +} - // Apply replacements sequentially - for (let i = 0; i < typedOptions.input.replacements.length; i++) { - const replacement = typedOptions.input.replacements[i]; +ToolRegistry.registerTool(MultiReplaceStringTool); - try { - // Validate individual replacement - if (!replacement.filePath || replacement.oldString === undefined || replacement.newString === undefined) { - throw new Error(`Invalid replacement at index ${i}: filePath, oldString, and newString are required`); - } +function textEditSorter(a: vscode.TextEdit, b: vscode.TextEdit) { + return b.range.end.compareTo(a.range.end) || b.range.start.compareTo(a.range.start); +} - // Create a new tool invocation options for this replacement - const replaceOptions = { - ...typedOptions, - input: replacement - }; - - // Set the prompt context for the replace tool - await replaceStringTool.resolveInput(replacement, this._promptContext); - - // Invoke the replace string tool - await replaceStringTool.invoke(replaceOptions as any, token); - - // Record success - results.results.push({ - operation: replacement, - success: true - }); - results.successfulReplacements++; - - } catch (error) { - // Record failure - const errorMessage = error instanceof Error ? error.message : String(error); - results.results.push({ - operation: replacement, - success: false, - error: errorMessage - }); - results.failedReplacements++; - - // Add error information to the stream using the correct method - (this._promptContext.stream as any).markdown(`\nāš ļø **Failed replacement ${i + 1}:**\n`); - (this._promptContext.stream as any).markdown(`- File: \`${replacement.filePath}\`\n`); - (this._promptContext.stream as any).markdown(`- Error: ${errorMessage}\n\n`); +/** + * Merge two arrays of notebook edits or text edits grouped by URI. + * Text edits for the same URI are concatenated and sorted in reverse file order (descending by start position). + */ +function mergeNotebookAndTextEdits(left: CellOrNotebookEdit[], right: CellOrNotebookEdit[]): CellOrNotebookEdit[] { + const notebookEdits: vscode.NotebookEdit[] = []; + const textEditsByUri = new ResourceMap(); + + const add = (item: vscode.NotebookEdit | [URI, vscode.TextEdit[]]) => { + if (Array.isArray(item)) { + const [uri, edits] = item; + let bucket = textEditsByUri.get(uri); + if (!bucket) { + bucket = []; + textEditsByUri.set(uri, bucket); } + bucket.push(...edits); + } else { + notebookEdits.push(item); } + }; - // Provide summary using the correct method - (this._promptContext.stream as any).markdown(`\n## Multi-Replace Summary\n\n`); - (this._promptContext.stream as any).markdown(`- **Total operations:** ${results.totalReplacements}\n`); - (this._promptContext.stream as any).markdown(`- **Successful:** ${results.successfulReplacements}\n`); - (this._promptContext.stream as any).markdown(`- **Failed:** ${results.failedReplacements}\n\n`); - - if (results.failedReplacements > 0) { - (this._promptContext.stream as any).markdown(`### Failed Operations:\n\n`); - results.results.filter(r => !r.success).forEach((result, index) => { - if (this._promptContext?.stream) { - (this._promptContext.stream as any).markdown(`${index + 1}. **${result.operation.filePath}**\n`); - (this._promptContext.stream as any).markdown(` - Error: ${result.error || 'Unknown error'}\n`); - (this._promptContext.stream as any).markdown(` - Old string: \`${result.operation.oldString.substring(0, 100)}${result.operation.oldString.length > 100 ? '...' : ''}\`\n\n`); - } - }); - } + left.forEach(add); + right.forEach(add); - // Return a simple result - return new LanguageModelToolResult([ - new LanguageModelTextPart( - `Multi-replace operation completed: ${results.successfulReplacements}/${results.totalReplacements} operations successful.` - ) - ]); + const mergedTextEditTuples: [URI, vscode.TextEdit[]][] = []; + for (const [uri, edits] of textEditsByUri.entries()) { + edits.sort(textEditSorter); + mergedTextEditTuples.push([uri, edits]); } - async resolveInput(input: IMultiReplaceStringToolParams, promptContext: IBuildPromptContext): Promise { - this._promptContext = promptContext; - return input; - } - - prepareInvocation(options: any, token: any): any { - return { - presentation: 'hidden' - }; - } + return [...notebookEdits, ...mergedTextEditTuples]; } - -ToolRegistry.registerTool(MultiReplaceStringTool); diff --git a/src/extension/tools/node/replaceStringTool.tsx b/src/extension/tools/node/replaceStringTool.tsx index 96fb23083..7a5aeaa4a 100644 --- a/src/extension/tools/node/replaceStringTool.tsx +++ b/src/extension/tools/node/replaceStringTool.tsx @@ -4,38 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import { CHAT_MODEL } from '../../../platform/configuration/common/configurationService'; -import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService'; -import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot'; -import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot'; -import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; -import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; -import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService'; -import { IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent'; -import { IAlternativeNotebookContentEditGenerator, NotebookEditGenerationTelemtryOptions, NotebookEditGenrationSource } from '../../../platform/notebook/common/alternativeContentEditGenerator'; -import { INotebookService } from '../../../platform/notebook/common/notebookService'; -import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; -import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; -import { ITelemetryService, multiplexProperties } from '../../../platform/telemetry/common/telemetry'; -import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; -import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl'; -import { removeLeadingFilepathComment } from '../../../util/common/markdown'; -import { timeout } from '../../../util/vs/base/common/async'; -import { URI } from '../../../util/vs/base/common/uri'; -import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { ChatResponseTextEditPart, EndOfLine, LanguageModelPromptTsxPart, LanguageModelToolResult, WorkspaceEdit } from '../../../vscodeTypes'; -import { IBuildPromptContext } from '../../prompt/common/intents'; -import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'; -import { processFullRewriteNotebook } from '../../prompts/node/codeMapper/codeMapper'; import { ToolName } from '../common/toolNames'; -import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry'; -import { IToolsService } from '../common/toolsService'; -import { ActionType } from './applyPatch/parser'; -import { CorrectedEditResult, healReplaceStringParams } from './editFileHealing'; -import { EditFileResult } from './editFileToolResult'; -import { EditError, NoChangeError, NoMatchError, applyEdit } from './editFileToolUtils'; -import { sendEditNotebookTelemetry } from './editNotebookTool'; -import { assertFileOkForTool, resolveToolInputPath } from './toolUtils'; +import { ToolRegistry } from '../common/toolsRegistry'; +import { AbstractReplaceStringTool } from './abstractReplaceStringTool'; export interface IReplaceStringToolParams { explanation: string; @@ -44,325 +15,16 @@ export interface IReplaceStringToolParams { newString: string; } -export class ReplaceStringTool implements ICopilotTool { +export class ReplaceStringTool extends AbstractReplaceStringTool { public static toolName = ToolName.ReplaceString; - private _promptContext: IBuildPromptContext | undefined; - - constructor( - @IPromptPathRepresentationService protected readonly promptPathRepresentationService: IPromptPathRepresentationService, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IWorkspaceService protected readonly workspaceService: IWorkspaceService, - @IToolsService protected readonly toolsService: IToolsService, - @INotebookService protected readonly notebookService: INotebookService, - @IFileSystemService protected readonly fileSystemService: IFileSystemService, - @IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService, - @IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, - @IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService, - @ILanguageDiagnosticsService private readonly languageDiagnosticsService: ILanguageDiagnosticsService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEndpointProvider private readonly endpointProvider: IEndpointProvider, - @IExperimentationService private readonly experimentationService: IExperimentationService - ) { } - async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken) { - const uri = resolveToolInputPath(options.input.filePath, this.promptPathRepresentationService); - try { - await this.instantiationService.invokeFunction(accessor => assertFileOkForTool(accessor, uri)); - } catch (error) { - this.sendReplaceTelemetry('invalidFile', options, undefined, undefined, undefined); - throw error; - } - - // Validate parameters - if (!options.input.filePath || options.input.oldString === undefined || options.input.newString === undefined || !this._promptContext?.stream) { - this.sendReplaceTelemetry('invalidStrings', options, undefined, undefined, undefined); - throw new Error('Invalid input'); - } - - const isNotebook = this.notebookService.hasSupportedNotebooks(uri); - const document = isNotebook ? - await this.workspaceService.openNotebookDocumentAndSnapshot(uri, this.alternativeNotebookContent.getFormat(this._promptContext?.request?.model)) : - await this.workspaceService.openTextDocumentAndSnapshot(uri); - - const existingDiagnostics = this.languageDiagnosticsService.getDiagnostics(document.uri); - - // String replacement mode - if (options.input.oldString !== undefined && options.input.newString !== undefined) { - - // Track edit survival - let editSurvivalTracker: IEditSurvivalTrackingSession | undefined; - let responseStream = this._promptContext.stream; - if (document && document instanceof TextDocumentSnapshot) { // Only for existing text documents - const tracker = editSurvivalTracker = this._editSurvivalTrackerService.initialize(document.document); - responseStream = ChatResponseStreamImpl.spy(this._promptContext.stream, (part) => { - if (part instanceof ChatResponseTextEditPart) { - tracker.collectAIEdits(part.edits); - } - }); - } - - const didHealRef = { didHeal: false }; - try { - const { workspaceEdit, updatedFile } = await this.generateEdit(uri, document, options, didHealRef, token); - - this._promptContext.stream.markdown('\n```\n'); - this._promptContext.stream.codeblockUri(uri, true); - - if (document instanceof NotebookDocumentSnapshot) { - const telemetryOptions: NotebookEditGenerationTelemtryOptions = { - model: options.model ? this.endpointProvider.getChatEndpoint(options.model).then(m => m.name) : undefined, - requestId: this._promptContext.requestId, - source: NotebookEditGenrationSource.stringReplace, - }; - this._promptContext.stream.notebookEdit(document.uri, []); - await processFullRewriteNotebook(document.document, updatedFile, this._promptContext.stream, this.alternativeNotebookEditGenerator, telemetryOptions, token); - this._promptContext.stream.notebookEdit(document.uri, true); - sendEditNotebookTelemetry(this.telemetryService, this.endpointProvider, 'stringReplace', document.uri, this._promptContext.requestId, options.model ?? this._promptContext.request?.model); - } else { - for (const [uri, edit] of workspaceEdit.entries()) { - responseStream.textEdit(uri, edit); - } - responseStream.textEdit(uri, true); - - timeout(2000).then(() => { - // The tool can't wait for edits to be applied, so just wait before starting the survival tracker. - // TODO@roblourens see if this improves the survival metric, find a better fix. - editSurvivalTracker?.startReporter(res => { - /* __GDPR__ - "codeMapper.trackEditSurvival" : { - "owner": "aeschli", - "comment": "Tracks how much percent of the AI edits survived after 5 minutes of accepting", - "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, - "requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" }, - "mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The code mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" }, - "survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the AI edit is still present in the document." }, - "survivalRateNoRevert": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the ranges the AI touched ended up being reverted." }, - "didBranchChange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Indicates if the branch changed in the meantime. If the branch changed (value is 1), this event should probably be ignored." }, - "timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." } - } - */ - res.telemetryService.sendMSFTTelemetryEvent('codeMapper.trackEditSurvival', { requestId: this._promptContext?.requestId, requestSource: 'agent', mapper: 'stringReplaceTool' }, { - survivalRateFourGram: res.fourGram, - survivalRateNoRevert: res.noRevert, - timeDelayMs: res.timeDelayMs, - didBranchChange: res.didBranchChange ? 1 : 0, - }); - }); - }); - } - - this._promptContext.stream.markdown('\n```\n'); - - void this.sendReplaceTelemetry('success', options, document.getText(), isNotebook, didHealRef.didHeal); - return new LanguageModelToolResult([ - new LanguageModelPromptTsxPart( - await renderPromptElementJSON( - this.instantiationService, - EditFileResult, - { files: [{ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics }], diagnosticsTimeout: 2000, toolName: ToolName.ReplaceString, requestId: options.chatRequestId, model: options.model }, - // If we are not called with tokenization options, have _some_ fake tokenizer - // otherwise we end up returning the entire document - options.tokenizationOptions ?? { - tokenBudget: 1000, - countTokens: (t) => Promise.resolve(t.length * 3 / 4) - }, - token, - ), - ) - ]); - - } catch (error) { - // Enhanced error message with more helpful details - let errorMessage = 'String replacement failed: '; - let outcome: string; - - if (error instanceof NoMatchError) { - outcome = options.input.oldString.match(/Lines \d+-\d+ omitted/) ? - 'oldStringHasOmittedLines' : - options.input.oldString.includes('{…}') ? - 'oldStringHasSummarizationMarker' : - options.input.oldString.includes('/*...*/') ? - 'oldStringHasSummarizationMarkerSemanticSearch' : - error.kindForTelemetry; - errorMessage += `${error.message}`; - } else if (error instanceof EditError) { - outcome = error.kindForTelemetry; - errorMessage += error.message; - } else { - outcome = 'other'; - errorMessage += `${error.message}`; - } - - void this.sendReplaceTelemetry(outcome, options, document.getText(), isNotebook, didHealRef.didHeal); - - // No edit, so no need to wait for diagnostics - const diagnosticsTimeout = 0; - return new LanguageModelToolResult([ - new LanguageModelPromptTsxPart( - await renderPromptElementJSON( - this.instantiationService, - EditFileResult, - { files: [{ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics, error: errorMessage }], diagnosticsTimeout, toolName: ToolName.ReplaceString, requestId: options.chatRequestId, model: options.model }, - options.tokenizationOptions ?? { - tokenBudget: 1000, - countTokens: (t) => Promise.resolve(t.length * 3 / 4) - }, - token, - ), - ) - ]); - } - } - } - - private async generateEdit(uri: URI, document: TextDocumentSnapshot | NotebookDocumentSnapshot, options: vscode.LanguageModelToolInvocationOptions, didHealRef: { didHeal: boolean }, token: vscode.CancellationToken) { - const filePath = this.promptPathRepresentationService.getFilePath(document.uri); - const eol = document instanceof TextDocumentSnapshot && document.eol === EndOfLine.CRLF ? '\r\n' : '\n'; - const oldString = removeLeadingFilepathComment(options.input.oldString, document.languageId, filePath).replace(/\r?\n/g, eol); - const newString = removeLeadingFilepathComment(options.input.newString, document.languageId, filePath).replace(/\r?\n/g, eol); - - // Apply the edit using the improved applyEdit function that uses VS Code APIs - const workspaceEdit = new WorkspaceEdit(); - let updatedFile: string; - try { - const result = await applyEdit( - uri, - oldString, - newString, - workspaceEdit, - this.workspaceService, - this.notebookService, - this.alternativeNotebookContent, - this._promptContext?.request?.model - ); - updatedFile = result.updatedFile; - } catch (e) { - if (!(e instanceof NoMatchError)) { - throw e; - } - - if (this.experimentationService.getTreatmentVariable('vscode', 'copilotchat.disableReplaceStringHealing') === true) { - throw e; // failsafe for next release. - } - - didHealRef.didHeal = true; - - let healed: CorrectedEditResult; - try { - healed = await healReplaceStringParams( - options.model, - document.getText(), - { - explanation: options.input.explanation, - filePath: filePath, - oldString, - newString, - }, - eol, - await this.endpointProvider.getChatEndpoint(CHAT_MODEL.GPT4OMINI), - token - ); - if (healed.params.oldString === healed.params.newString) { - throw new NoChangeError('change was identical after healing', document.uri.fsPath); - } - } catch (e2) { - this.sendHealingTelemetry(options, String(e2), undefined); - throw e; // original error - } - - try { - const result = await applyEdit( - uri, - healed.params.oldString, - healed.params.newString, - workspaceEdit, - this.workspaceService, - this.notebookService, - this.alternativeNotebookContent, - this._promptContext?.request?.model - ); - updatedFile = result.updatedFile; - } catch (e2) { - this.sendHealingTelemetry(options, undefined, String(e2)); - throw e; // original error - } - } - - return { workspaceEdit, updatedFile }; - } - - private async sendReplaceTelemetry(outcome: string, options: vscode.LanguageModelToolInvocationOptions, file: string | undefined, isNotebookDocument: boolean | undefined, didHeal: boolean | undefined) { - const model = await this.modelForTelemetry(options); - const isNotebook = isNotebookDocument ? 1 : (isNotebookDocument === false ? 0 : -1); - /* __GDPR__ - "replaceStringToolInvoked" : { - "owner": "roblourens", - "comment": "The replace_string tool was invoked", - "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, - "interactionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current interaction." }, - "outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the invocation was successful, or a failure reason" }, - "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }, - "isNotebook": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." }, - "didHeal": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." } - } - */ - this.telemetryService.sendMSFTTelemetryEvent('replaceStringToolInvoked', - { - requestId: options.chatRequestId, - interactionId: options.chatRequestId, - outcome, - model - }, { isNotebook, didHeal: didHeal === undefined ? -1 : (didHeal ? 1 : 0) } - ); - - this.telemetryService.sendEnhancedGHTelemetryEvent('replaceStringTool', multiplexProperties({ - headerRequestId: options.chatRequestId, - baseModel: model, - messageText: file, - completionTextJson: JSON.stringify(options.input), - postProcessingOutcome: outcome, - }), { isNotebook }); - } - - private async sendHealingTelemetry(options: vscode.LanguageModelToolInvocationOptions, healError: string | undefined, applicationError: string | undefined) { - /* __GDPR__ - "replaceStringHealingStat" : { - "owner": "roblourens", - "comment": "The replace_string tool was invoked", - "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." }, - "interactionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current interaction." }, - "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" }, - "outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the invocation was successful, or a failure reason" }, - "healError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Any error that happened during healing" }, - "applicationError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Any error that happened after application" }, - "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook, 1 = yes, 0 = no, other = unknown." } - } - */ - this.telemetryService.sendMSFTTelemetryEvent('replaceStringHealingStat', - { - requestId: options.chatRequestId, - interactionId: options.chatRequestId, - model: await this.modelForTelemetry(options), - healError, - applicationError, - }, { success: healError === undefined && applicationError === undefined ? 1 : 0 } - ); - } - - private async modelForTelemetry(options: vscode.LanguageModelToolInvocationOptions) { - return options.model && (await this.endpointProvider.getChatEndpoint(options.model)).model; - } - - async resolveInput(input: IReplaceStringToolParams, promptContext: IBuildPromptContext): Promise { - this._promptContext = promptContext; // TODO@joyceerhl @roblourens HACK: Avoid types in the input being serialized and not deserialized when they go through invokeTool - return input; + const prepared = await this.prepareEditsForFile(options, options.input, token); + return this.applyAllEdits(options, [prepared], token); } - prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions, token: vscode.CancellationToken): vscode.ProviderResult { - return { - presentation: 'hidden' - }; + protected override toolName(): ToolName { + return ReplaceStringTool.toolName; } } diff --git a/src/extension/tools/node/test/editFileToolUtils.spec.ts b/src/extension/tools/node/test/editFileToolUtils.spec.ts index cc6e803ca..2c5c9e1db 100644 --- a/src/extension/tools/node/test/editFileToolUtils.spec.ts +++ b/src/extension/tools/node/test/editFileToolUtils.spec.ts @@ -23,8 +23,10 @@ describe('replace_string_in_file - applyEdit', () => { let alternatveContentService: IAlternativeNotebookContentService; let doc: ExtHostDocumentData; - function doApplyEdit(oldString: string, newString: string, uri = doc.document.uri) { - return applyEdit(uri, oldString, newString, workspaceEdit, workspaceService, notebookService as INotebookService, alternatveContentService, undefined); + async function doApplyEdit(oldString: string, newString: string, uri = doc.document.uri) { + const r = await applyEdit(uri, oldString, newString, workspaceService, notebookService as INotebookService, alternatveContentService, undefined); + workspaceEdit.set(uri, r.edits); + return r; } function setText(value: string) {