Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts
5272 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { match, splitGlobAware } from '../../../../../base/common/glob.js';
8
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { basename, dirname } from '../../../../../base/common/resources.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { localize } from '../../../../../nls.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { IFileService } from '../../../../../platform/files/common/files.js';
15
import { ILabelService } from '../../../../../platform/label/common/label.js';
16
import { ILogService } from '../../../../../platform/log/common/log.js';
17
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
18
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
19
import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../attachments/chatVariableEntries.js';
20
import { ILanguageModelToolsService, IToolData, VSCodeToolReference } from '../tools/languageModelToolsService.js';
21
import { PromptsConfig } from './config/config.js';
22
import { isInClaudeRulesFolder, isPromptOrInstructionsFile } from './config/promptFileLocations.js';
23
import { PromptsType } from './promptTypes.js';
24
import { ParsedPromptFile } from './promptFileParser.js';
25
import { AgentFileType, ICustomAgent, IPromptPath, IPromptsService } from './service/promptsService.js';
26
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
27
import { ChatConfiguration, ChatModeKind } from '../constants.js';
28
import { UserSelectedTools } from '../participants/chatAgents.js';
29
30
export type InstructionsCollectionEvent = {
31
applyingInstructionsCount: number;
32
referencedInstructionsCount: number;
33
agentInstructionsCount: number;
34
listedInstructionsCount: number;
35
totalInstructionsCount: number;
36
};
37
export function newInstructionsCollectionEvent(): InstructionsCollectionEvent {
38
return { applyingInstructionsCount: 0, referencedInstructionsCount: 0, agentInstructionsCount: 0, listedInstructionsCount: 0, totalInstructionsCount: 0 };
39
}
40
41
type InstructionsCollectionClassification = {
42
applyingInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of instructions added via pattern matching.' };
43
referencedInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of instructions added via references from other instruction files.' };
44
agentInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of agent instructions added (copilot-instructions.md and agents.md).' };
45
listedInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of instruction patterns added.' };
46
totalInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Total number of instruction entries added to variables.' };
47
owner: 'digitarald';
48
comment: 'Tracks automatic instruction collection usage in chat prompt system.';
49
};
50
51
export class ComputeAutomaticInstructions {
52
53
private _parseResults: ResourceMap<ParsedPromptFile> = new ResourceMap();
54
55
constructor(
56
private readonly _modeKind: ChatModeKind,
57
private readonly _enabledTools: UserSelectedTools | undefined,
58
private readonly _enabledSubagents: (readonly string[]) | undefined,
59
@IPromptsService private readonly _promptsService: IPromptsService,
60
@ILogService public readonly _logService: ILogService,
61
@ILabelService private readonly _labelService: ILabelService,
62
@IConfigurationService private readonly _configurationService: IConfigurationService,
63
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
64
@IFileService private readonly _fileService: IFileService,
65
@ITelemetryService private readonly _telemetryService: ITelemetryService,
66
@ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService,
67
) {
68
}
69
70
private async _parseInstructionsFile(uri: URI, token: CancellationToken): Promise<ParsedPromptFile | undefined> {
71
if (this._parseResults.has(uri)) {
72
return this._parseResults.get(uri)!;
73
}
74
try {
75
const result = await this._promptsService.parseNew(uri, token);
76
this._parseResults.set(uri, result);
77
return result;
78
} catch (error) {
79
this._logService.error(`[InstructionsContextComputer] Failed to parse instruction file: ${uri}`, error);
80
return undefined;
81
}
82
83
}
84
85
public async collect(variables: ChatRequestVariableSet, token: CancellationToken): Promise<void> {
86
87
const instructionFiles = await this._promptsService.listPromptFiles(PromptsType.instructions, token);
88
89
this._logService.trace(`[InstructionsContextComputer] ${instructionFiles.length} instruction files available.`);
90
91
const telemetryEvent: InstructionsCollectionEvent = newInstructionsCollectionEvent();
92
const context = this._getContext(variables);
93
94
// find instructions where the `applyTo` matches the attached context
95
await this.addApplyingInstructions(instructionFiles, context, variables, telemetryEvent, token);
96
97
// add all instructions referenced by all instruction files that are in the context
98
await this._addReferencedInstructions(variables, telemetryEvent, token);
99
100
// get copilot instructions
101
await this._addAgentInstructions(variables, telemetryEvent, token);
102
103
const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, token);
104
if (instructionsListVariable) {
105
variables.add(instructionsListVariable);
106
telemetryEvent.listedInstructionsCount++;
107
}
108
109
this.sendTelemetry(telemetryEvent);
110
}
111
112
private sendTelemetry(telemetryEvent: InstructionsCollectionEvent): void {
113
// Emit telemetry
114
telemetryEvent.totalInstructionsCount = telemetryEvent.agentInstructionsCount + telemetryEvent.referencedInstructionsCount + telemetryEvent.applyingInstructionsCount + telemetryEvent.listedInstructionsCount;
115
this._telemetryService.publicLog2<InstructionsCollectionEvent, InstructionsCollectionClassification>('instructionsCollected', telemetryEvent);
116
}
117
118
/** public for testing */
119
public async addApplyingInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise<void> {
120
const includeApplyingInstructions = this._configurationService.getValue(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS);
121
if (!includeApplyingInstructions && this._modeKind !== ChatModeKind.Edit) {
122
this._logService.trace(`[InstructionsContextComputer] includeApplyingInstructions is disabled and agent kind is not Edit. No applying instructions will be added.`);
123
return;
124
}
125
126
for (const { uri } of instructionFiles) {
127
const parsedFile = await this._parseInstructionsFile(uri, token);
128
if (!parsedFile) {
129
this._logService.trace(`[InstructionsContextComputer] Unable to read: ${uri}`);
130
continue;
131
}
132
133
const applyTo = parsedFile.header?.applyTo;
134
const paths = parsedFile.header?.paths;
135
136
// Claude rules files use `paths` (defaulting to '**' when omitted),
137
// regular instruction files use `applyTo` (skipped when omitted)
138
const isClaudeRules = isInClaudeRulesFolder(uri);
139
const pattern = isClaudeRules ? (paths?.join(', ') ?? '**') : applyTo;
140
141
if (!pattern) {
142
this._logService.trace(`[InstructionsContextComputer] No 'applyTo' found: ${uri}`);
143
continue;
144
}
145
146
if (context.instructions.has(uri)) {
147
// the instruction file is already part of the input or has already been processed
148
this._logService.trace(`[InstructionsContextComputer] Skipping already processed instruction file: ${uri}`);
149
continue;
150
}
151
152
const match = this._matches(context.files, pattern);
153
if (match) {
154
this._logService.trace(`[InstructionsContextComputer] Match for ${uri} with ${match.pattern}${match.file ? ` for file ${match.file}` : ''}`);
155
156
const reason = !match.file ?
157
localize('instruction.file.reason.allFiles', 'Automatically attached as pattern is **') :
158
localize('instruction.file.reason.specificFile', 'Automatically attached as pattern {0} matches {1}', pattern, this._labelService.getUriLabel(match.file, { relative: true }));
159
160
variables.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason, true));
161
telemetryEvent.applyingInstructionsCount++;
162
} else {
163
this._logService.trace(`[InstructionsContextComputer] No match for ${uri} with ${pattern}`);
164
}
165
}
166
}
167
168
private _getContext(attachedContext: ChatRequestVariableSet): { files: ResourceSet; instructions: ResourceSet } {
169
const files = new ResourceSet();
170
const instructions = new ResourceSet();
171
for (const variable of attachedContext.asArray()) {
172
if (isPromptFileVariableEntry(variable)) {
173
instructions.add(variable.value);
174
} else {
175
const uri = IChatRequestVariableEntry.toUri(variable);
176
if (uri) {
177
files.add(uri);
178
}
179
}
180
}
181
182
return { files, instructions };
183
}
184
185
private async _addAgentInstructions(variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise<void> {
186
const logger = {
187
logInfo: (message: string) => this._logService.trace(`[InstructionsContextComputer] ${message}`)
188
};
189
const allCandidates = await this._promptsService.listAgentInstructions(token, logger);
190
191
const entries: ChatRequestVariableSet = new ChatRequestVariableSet();
192
const copilotEntries: ChatRequestVariableSet = new ChatRequestVariableSet();
193
194
for (const { uri, type } of allCandidates) {
195
const varEntry = toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, undefined, true);
196
entries.add(varEntry);
197
if (type === AgentFileType.copilotInstructionsMd) {
198
copilotEntries.add(varEntry);
199
}
200
201
telemetryEvent.agentInstructionsCount++;
202
logger.logInfo(`Agent instruction file added: ${uri.toString()}`);
203
}
204
205
// Process referenced instructions from copilot files (maintaining original behavior)
206
if (copilotEntries.length > 0) {
207
await this._addReferencedInstructions(copilotEntries, telemetryEvent, token);
208
for (const entry of copilotEntries.asArray()) {
209
variables.add(entry);
210
}
211
}
212
213
for (const entry of entries.asArray()) {
214
variables.add(entry);
215
}
216
}
217
218
/**
219
* Combines the `applyTo` and `paths` attributes into a single comma-separated
220
* pattern string that can be matched by {@link _matches}.
221
* Used for the instructions list XML output where both should be shown.
222
*/
223
private _getApplyToPattern(applyTo: string | undefined, paths: readonly string[] | undefined): string | undefined {
224
if (applyTo) {
225
return applyTo;
226
}
227
if (paths && paths.length > 0) {
228
return paths.join(', ');
229
}
230
return undefined;
231
}
232
233
private _matches(files: ResourceSet, applyToPattern: string): { pattern: string; file?: URI } | undefined {
234
const patterns = splitGlobAware(applyToPattern, ',');
235
const patterMatches = (pattern: string): { pattern: string; file?: URI } | undefined => {
236
pattern = pattern.trim();
237
if (pattern.length === 0) {
238
// if glob pattern is empty, skip it
239
return undefined;
240
}
241
if (pattern === '**' || pattern === '**/*' || pattern === '*') {
242
// if glob pattern is one of the special wildcard values,
243
// add the instructions file event if no files are attached
244
return { pattern };
245
}
246
if (!pattern.startsWith('/') && !pattern.startsWith('**/')) {
247
// support relative glob patterns, e.g. `src/**/*.js`
248
pattern = '**/' + pattern;
249
}
250
251
// match each attached file with each glob pattern and
252
// add the instructions file if its rule matches the file
253
for (const file of files) {
254
// if the file is not a valid URI, skip it
255
if (match(pattern, file.path, { ignoreCase: true })) {
256
return { pattern, file }; // return the matched pattern and file URI
257
}
258
}
259
return undefined;
260
};
261
for (const pattern of patterns) {
262
const matchResult = patterMatches(pattern);
263
if (matchResult) {
264
return matchResult; // return the first matched pattern and file URI
265
}
266
}
267
return undefined;
268
}
269
270
private _getTool(referenceName: string): { tool: IToolData; variable: string } | undefined {
271
if (!this._enabledTools) {
272
return undefined;
273
}
274
const tool = this._languageModelToolsService.getToolByName(referenceName);
275
if (tool && this._enabledTools[tool.id]) {
276
return { tool, variable: `#tool:${this._languageModelToolsService.getFullReferenceName(tool)}` };
277
}
278
return undefined;
279
}
280
281
private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise<IPromptTextVariableEntry | undefined> {
282
const readTool = this._getTool('readFile');
283
const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent);
284
285
const entries: string[] = [];
286
if (readTool) {
287
288
const searchNestedAgentMd = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD);
289
const agentsMdPromise = searchNestedAgentMd ? this._promptsService.listNestedAgentMDs(token) : Promise.resolve([]);
290
291
entries.push('<instructions>');
292
entries.push('Here is a list of instruction files that contain rules for working with this codebase.');
293
entries.push('These files are important for understanding the codebase structure, conventions, and best practices.');
294
entries.push('Please make sure to follow the rules specified in these files when working with the codebase.');
295
entries.push(`If the file is not already available as attachment, use the ${readTool.variable} tool to acquire it.`);
296
entries.push('Make sure to acquire the instructions before working with the codebase.');
297
let hasContent = false;
298
for (const { uri } of instructionFiles) {
299
const parsedFile = await this._parseInstructionsFile(uri, token);
300
if (parsedFile) {
301
entries.push('<instruction>');
302
if (parsedFile.header) {
303
const { description, applyTo, paths } = parsedFile.header;
304
if (description) {
305
entries.push(`<description>${description}</description>`);
306
}
307
entries.push(`<file>${getFilePath(uri)}</file>`);
308
const applyToPattern = this._getApplyToPattern(applyTo, paths);
309
if (applyToPattern) {
310
entries.push(`<applyTo>${applyToPattern}</applyTo>`);
311
}
312
} else {
313
entries.push(`<file>${getFilePath(uri)}</file>`);
314
}
315
entries.push('</instruction>');
316
hasContent = true;
317
}
318
}
319
320
const agentsMdFiles = await agentsMdPromise;
321
for (const { uri } of agentsMdFiles) {
322
const folderName = this._labelService.getUriLabel(dirname(uri), { relative: true });
323
const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName);
324
entries.push('<instruction>');
325
entries.push(`<description>${description}</description>`);
326
entries.push(`<file>${getFilePath(uri)}</file>`);
327
entries.push('</instruction>');
328
hasContent = true;
329
330
}
331
332
if (!hasContent) {
333
entries.length = 0; // clear entries
334
} else {
335
entries.push('</instructions>', '', ''); // add trailing newline
336
}
337
338
const agentSkills = await this._promptsService.findAgentSkills(token);
339
// Filter out skills with disableModelInvocation=true (they can only be triggered manually via /name)
340
const modelInvokableSkills = agentSkills?.filter(skill => !skill.disableModelInvocation);
341
if (modelInvokableSkills && modelInvokableSkills.length > 0) {
342
const useSkillAdherencePrompt = this._configurationService.getValue(PromptsConfig.USE_SKILL_ADHERENCE_PROMPT);
343
entries.push('<skills>');
344
if (useSkillAdherencePrompt) {
345
// Stronger skill adherence prompt for experimental feature
346
entries.push('Skills provide specialized capabilities, domain knowledge, and refined workflows for producing high-quality outputs. Each skill folder contains tested instructions for specific domains like testing strategies, API design, or performance optimization. Multiple skills can be combined when a task spans different domains.');
347
entries.push(`BLOCKING REQUIREMENT: When a skill applies to the user's request, you MUST load and read the SKILL.md file IMMEDIATELY as your first action, BEFORE generating any other response or taking action on the task. Use ${readTool.variable} to load the relevant skill(s).`);
348
entries.push('NEVER just mention or reference a skill in your response without actually reading it first. If a skill is relevant, load it before proceeding.');
349
entries.push('How to determine if a skill applies:');
350
entries.push('1. Review the available skills below and match their descriptions against the user\'s request');
351
entries.push('2. If any skill\'s domain overlaps with the task, load that skill immediately');
352
entries.push('3. When multiple skills apply (e.g., a flowchart in documentation), load all relevant skills');
353
entries.push('Examples:');
354
entries.push(`- "Help me write unit tests for this module" -> Load the testing skill via ${readTool.variable} FIRST, then proceed`);
355
entries.push(`- "Optimize this slow function" -> Load the performance-profiling skill via ${readTool.variable} FIRST, then proceed`);
356
entries.push(`- "Add a discount code field to checkout" -> Load both the checkout-flow and form-validation skills FIRST`);
357
entries.push('Available skills:');
358
} else {
359
entries.push('Here is a list of skills that contain domain specific knowledge on a variety of topics.');
360
entries.push('Each skill comes with a description of the topic and a file path that contains the detailed instructions.');
361
entries.push(`When a user asks you to perform a task that falls within the domain of a skill, use the ${readTool.variable} tool to acquire the full instructions from the file URI.`);
362
}
363
for (const skill of modelInvokableSkills) {
364
entries.push('<skill>');
365
entries.push(`<name>${skill.name}</name>`);
366
if (skill.description) {
367
entries.push(`<description>${skill.description}</description>`);
368
}
369
entries.push(`<file>${getFilePath(skill.uri)}</file>`);
370
entries.push('</skill>');
371
}
372
entries.push('</skills>', '', ''); // add trailing newline
373
}
374
}
375
if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) {
376
const canUseAgent = (() => {
377
if (!this._enabledSubagents || this._enabledSubagents.includes('*')) {
378
return (agent: ICustomAgent) => agent.visibility.agentInvokable;
379
} else {
380
const subagents = this._enabledSubagents;
381
return (agent: ICustomAgent) => subagents.includes(agent.name);
382
}
383
})();
384
const agents = await this._promptsService.getCustomAgents(token);
385
if (agents.length > 0) {
386
entries.push('<agents>');
387
entries.push('Here is a list of agents that can be used when running a subagent.');
388
entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.');
389
entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`);
390
for (const agent of agents) {
391
if (canUseAgent(agent)) {
392
entries.push('<agent>');
393
entries.push(`<name>${agent.name}</name>`);
394
if (agent.description) {
395
entries.push(`<description>${agent.description}</description>`);
396
}
397
if (agent.argumentHint) {
398
entries.push(`<argumentHint>${agent.argumentHint}</argumentHint>`);
399
}
400
entries.push('</agent>');
401
}
402
}
403
entries.push('</agents>', '', ''); // add trailing newline
404
}
405
}
406
if (entries.length === 0) {
407
return undefined;
408
}
409
410
const content = entries.join('\n');
411
const toolReferences: ChatRequestToolReferenceEntry[] = [];
412
const collectToolReference = (tool: { tool: IToolData; variable: string } | undefined) => {
413
if (tool) {
414
let offset = content.indexOf(tool.variable);
415
while (offset >= 0) {
416
toolReferences.push(toToolVariableEntry(tool.tool, new OffsetRange(offset, offset + tool.variable.length)));
417
offset = content.indexOf(tool.variable, offset + 1);
418
}
419
}
420
};
421
collectToolReference(readTool);
422
collectToolReference(runSubagentTool);
423
return toPromptTextVariableEntry(content, true, toolReferences);
424
}
425
426
private async _addReferencedInstructions(attachedContext: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise<void> {
427
const includeReferencedInstructions = this._configurationService.getValue(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS);
428
if (!includeReferencedInstructions && this._modeKind !== ChatModeKind.Edit) {
429
this._logService.trace(`[InstructionsContextComputer] includeReferencedInstructions is disabled and agent kind is not Edit. No referenced instructions will be added.`);
430
return;
431
}
432
433
const seen = new ResourceSet();
434
const todo: URI[] = [];
435
for (const variable of attachedContext.asArray()) {
436
if (isPromptFileVariableEntry(variable)) {
437
if (!seen.has(variable.value)) {
438
todo.push(variable.value);
439
seen.add(variable.value);
440
}
441
}
442
}
443
let next = todo.pop();
444
while (next) {
445
const result = await this._parseInstructionsFile(next, token);
446
if (result && result.body) {
447
const refsToCheck: { resource: URI }[] = [];
448
for (const ref of result.body.fileReferences) {
449
const url = result.body.resolveFilePath(ref.content);
450
if (url && !seen.has(url) && (isPromptOrInstructionsFile(url) || this._workspaceService.getWorkspaceFolder(url) !== undefined)) {
451
// only add references that are either prompt or instruction files or are part of the workspace
452
refsToCheck.push({ resource: url });
453
seen.add(url);
454
}
455
}
456
if (refsToCheck.length > 0) {
457
const stats = await this._fileService.resolveAll(refsToCheck);
458
for (let i = 0; i < stats.length; i++) {
459
const stat = stats[i];
460
const uri = refsToCheck[i].resource;
461
if (stat.success && stat.stat?.isFile) {
462
if (isPromptOrInstructionsFile(uri)) {
463
// only recursively parse instruction files
464
todo.push(uri);
465
}
466
const reason = localize('instruction.file.reason.referenced', 'Referenced by {0}', basename(next));
467
attachedContext.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.InstructionReference, reason, true));
468
telemetryEvent.referencedInstructionsCount++;
469
this._logService.trace(`[InstructionsContextComputer] ${uri.toString()} added, referenced by ${next.toString()}`);
470
}
471
}
472
}
473
}
474
next = todo.pop();
475
}
476
}
477
}
478
479
480
function getFilePath(uri: URI): string {
481
if (uri.scheme === Schemas.file || uri.scheme === Schemas.vscodeRemote) {
482
return uri.fsPath;
483
}
484
return uri.toString();
485
}
486
487