Skip to content

Commit 68b8c67

Browse files
rwollCopilot
andcommitted
Fix edit tools for headless tool invocation (scenario automation)
Edit tools (apply_patch, create_file, replace_string) fail when invoked via vscode.lm.invokeTool() outside a chat participant context because resolveInput() is never called and this._promptContext?.stream is undefined. This unblocks the vscode-copilot-evaluation tool call service mode where an external process invokes tools via HTTP without a VS Code chat session. For each tool, when no chat response stream is available (!hasStream): - apply_patch: Apply the already-built WorkspaceEdit directly via workspaceService.applyEdit() instead of streaming through responseStream - create_file: Write file directly via fileSystemService.writeFile() - replace_string: Build a WorkspaceEdit from the generated text edits and apply directly The existing stream-based code path is completely unchanged. The !hasStream branches return early before any stream code is reached. Fixes microsoft/vscode-copilot-evaluation#2818 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7814486 commit 68b8c67

File tree

7 files changed

+301
-65
lines changed

7 files changed

+301
-65
lines changed

package-lock.json

Lines changed: 2 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/tools/node/abstractReplaceStringTool.tsx

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { NotebookDocumentSnapshot } from '../../../platform/editing/common/noteb
1010
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
1111
import { modelShouldUseReplaceStringHealing } from '../../../platform/endpoint/common/chatModelCapabilities';
1212
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
13+
import { isScenarioAutomation } from '../../../platform/env/common/envService';
1314
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
1415
import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService';
1516
import { ILogService } from '../../../platform/log/common/logService';
@@ -28,7 +29,7 @@ import { extUriBiasedIgnorePathCase } from '../../../util/vs/base/common/resourc
2829
import { isDefined } from '../../../util/vs/base/common/types';
2930
import { URI } from '../../../util/vs/base/common/uri';
3031
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
31-
import { ChatRequestEditorData, ChatResponseTextEditPart, EndOfLine, ExtendedLanguageModelToolResult, Position as ExtPosition, LanguageModelPromptTsxPart, LanguageModelToolResult, TextEdit } from '../../../vscodeTypes';
32+
import { ChatRequestEditorData, ChatResponseTextEditPart, EndOfLine, ExtendedLanguageModelToolResult, Position as ExtPosition, LanguageModelPromptTsxPart, LanguageModelToolResult, TextEdit, WorkspaceEdit } from '../../../vscodeTypes';
3233
import { IBuildPromptContext } from '../../prompt/common/intents';
3334
import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer';
3435
import { CellOrNotebookEdit, processFullRewriteNotebookEdits } from '../../prompts/node/codeMapper/codeMapper';
@@ -173,7 +174,7 @@ export abstract class AbstractReplaceStringTool<T extends { explanation: string
173174
}
174175

175176
// Validate parameters
176-
if (!input.filePath || input.oldString === undefined || input.newString === undefined || !this._promptContext) {
177+
if (!input.filePath || input.oldString === undefined || input.newString === undefined || (!this._promptContext && !isScenarioAutomation)) {
177178
this.sendReplaceTelemetry('invalidStrings', options, input, undefined, undefined, undefined);
178179
throw new Error('Invalid input');
179180
}
@@ -207,12 +208,12 @@ export abstract class AbstractReplaceStringTool<T extends { explanation: string
207208
const model = await this.modelForTelemetry(options);
208209
const telemetryOptions: NotebookEditGenerationTelemtryOptions = {
209210
model,
210-
requestId: this._promptContext.requestId,
211+
requestId: this._promptContext?.requestId,
211212
source: NotebookEditGenrationSource.stringReplace,
212213
};
213214

214215
notebookEdits = await Iterable.asyncToArray(processFullRewriteNotebookEdits(document.document, updatedFile, this.alternativeNotebookEditGenerator, telemetryOptions, token));
215-
sendEditNotebookTelemetry(this.telemetryService, this.endpointProvider, 'stringReplace', document.uri, this._promptContext.requestId, model || 'unknown');
216+
sendEditNotebookTelemetry(this.telemetryService, this.endpointProvider, 'stringReplace', document.uri, this._promptContext?.requestId, model || 'unknown');
216217
updated = NotebookDocumentSnapshot.fromNewText(updatedFile, document);
217218
} else {
218219
updated = TextDocumentSnapshot.fromNewText(updatedFile, document);
@@ -255,12 +256,67 @@ export abstract class AbstractReplaceStringTool<T extends { explanation: string
255256
}
256257

257258
protected async applyAllEdits(options: vscode.LanguageModelToolInvocationOptions<T>, edits: IPrepareEdit[], token: vscode.CancellationToken) {
258-
if (!this._promptContext?.stream) {
259+
const hasStream = !!this._promptContext?.stream;
260+
if (!hasStream && !isScenarioAutomation) {
259261
throw new Error('no prompt context found');
260262
}
261263

262264
logEditToolResult(this.logService, options.chatRequestId, ...edits.map(e => ({ input: e.input, success: e.generatedEdit.success, healed: e.healed })));
263265

266+
// Scenario automation / headless mode: apply edits directly without streaming
267+
if (!hasStream) {
268+
const fileResults: IEditedFile[] = [];
269+
const workspaceEdit = new WorkspaceEdit();
270+
271+
for (const { document, uri, generatedEdit, healed } of edits) {
272+
const isNotebook = this.notebookService.hasSupportedNotebooks(uri);
273+
const existingDiagnostics = document ? this.languageDiagnosticsService.getDiagnostics(document.uri) : [];
274+
275+
if (!generatedEdit.success) {
276+
fileResults.push({ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics, error: generatedEdit.errorMessage });
277+
continue;
278+
}
279+
280+
if (generatedEdit.textEdits) {
281+
for (const edit of generatedEdit.textEdits) {
282+
workspaceEdit.replace(uri, edit.range, edit.newText);
283+
}
284+
}
285+
286+
fileResults.push({
287+
operation: ActionType.UPDATE,
288+
uri,
289+
isNotebook,
290+
existingDiagnostics,
291+
healed: healed ? JSON.stringify({ oldString: healed.oldString, newString: healed.newString }, null, 2) : undefined
292+
});
293+
}
294+
295+
await this.workspaceService.applyEdit(workspaceEdit);
296+
297+
const result = new ExtendedLanguageModelToolResult([
298+
new LanguageModelPromptTsxPart(
299+
await renderPromptElementJSON(
300+
this.instantiationService,
301+
EditFileResult,
302+
{ files: fileResults, diagnosticsTimeout: 2000, toolName: this.toolName(), requestId: options.chatRequestId, model: options.model },
303+
options.tokenizationOptions ?? {
304+
tokenBudget: 5000,
305+
countTokens: (t) => Promise.resolve(t.length * 3 / 4)
306+
},
307+
token,
308+
),
309+
)
310+
]);
311+
result.hasError = fileResults.some(f => f.error);
312+
return result;
313+
}
314+
315+
// Stream path: _promptContext is guaranteed to exist since hasStream is true
316+
if (!this._promptContext?.stream) {
317+
throw new Error('no prompt context found');
318+
}
319+
264320
const fileResults: IEditedFile[] = [];
265321
const existingDiagnosticMap = new ResourceMap<vscode.Diagnostic[]>();
266322

0 commit comments

Comments
 (0)