Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts
13401 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, CancellationTokenSource } from '../../../../base/common/cancellation.js';
7
import { Disposable } from '../../../../base/common/lifecycle.js';
8
import { OS } from '../../../../base/common/platform.js';
9
import { generateUuid } from '../../../../base/common/uuid.js';
10
import { localize } from '../../../../nls.js';
11
import { ILogService } from '../../../../platform/log/common/log.js';
12
import { IWorkbenchContribution } from '../../../common/contributions.js';
13
import { IChatDebugCustomizationLogEntry, IChatDebugEventFileListContent, IChatDebugResolvedEventContent, IChatDebugService } from '../common/chatDebugService.js';
14
import { IChatAgentService } from '../common/participants/chatAgents.js';
15
import { IChatService } from '../common/chatService/chatService.js';
16
import { ChatRequestHooks, formatHookCommandLabel } from '../common/promptSyntax/hookSchema.js';
17
import { HookType } from '../common/promptSyntax/hookTypes.js';
18
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
19
import { IHookDiscoveryInfo, type InstructionsCollectionDebugInfo, IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
20
import { lastInstructionsCollectionResult } from '../common/promptSyntax/computeAutomaticInstructions.js';
21
22
interface ICustomizationEventData {
23
readonly debugInfo: InstructionsCollectionDebugInfo;
24
readonly hooks: ChatRequestHooks | undefined;
25
}
26
27
/**
28
* Bridges prompt discovery information to {@link IChatDebugService}.
29
*/
30
export class PromptsDebugContribution extends Disposable implements IWorkbenchContribution {
31
32
static readonly ID = 'workbench.contrib.promptsDebug';
33
34
private static readonly MAX_DISCOVERY_DETAILS = 10_000;
35
36
/**
37
* Maps debug event IDs to their discovery info, so that
38
* {@link IChatDebugService.resolveEvent} can return rich details.
39
*/
40
private readonly _discoveryEventDetails = new Map<string, IPromptDiscoveryInfo>();
41
private readonly _customizationEventDetails = new Map<string, ICustomizationEventData>();
42
private readonly _loggedSessions = new Set<string>();
43
44
constructor(
45
@IPromptsService private readonly promptsService: IPromptsService,
46
@IChatAgentService chatAgentService: IChatAgentService,
47
@IChatService chatService: IChatService,
48
@IChatDebugService chatDebugService: IChatDebugService,
49
@ILogService logService: ILogService,
50
) {
51
super();
52
53
// Clean up logged-session entries when sessions are disposed.
54
this._register(chatService.onDidDisposeSession(e => {
55
for (const sessionResource of e.sessionResources) {
56
this._loggedSessions.delete(sessionResource.toString());
57
}
58
}));
59
60
// Forward discovery log events to the debug service.
61
this._register(chatAgentService.onWillInvokeAgent(async e => {
62
const sessionKey = e.request.sessionResource.toString();
63
const isFirstInvocation = !this._loggedSessions.has(sessionKey);
64
this._loggedSessions.add(sessionKey);
65
66
const sessionResource = e.request.sessionResource;
67
68
if (isFirstInvocation) {
69
const cts = new CancellationTokenSource();
70
try {
71
const discoveryInfos = await Promise.all([PromptsType.agent, PromptsType.instructions, PromptsType.prompt, PromptsType.skill, PromptsType.hook].map(type => this.promptsService.getDiscoveryInfo(type, cts.token)));
72
for (const discoveryInfo of discoveryInfos) {
73
const { name, details } = this.getDiscoveryLogEntry(discoveryInfo);
74
const eventId = generateUuid();
75
76
this._discoveryEventDetails.set(eventId, discoveryInfo);
77
78
// Evict oldest entries when the map exceeds the cap.
79
if (this._discoveryEventDetails.size > PromptsDebugContribution.MAX_DISCOVERY_DETAILS) {
80
const first = this._discoveryEventDetails.keys().next().value;
81
if (first !== undefined) {
82
this._discoveryEventDetails.delete(first);
83
}
84
}
85
86
// Enrich details with file paths so they appear in the event
87
// payload (e.g. forwarded via onDidReceiveChatDebugEvent to the
88
// extension's JSONL file logger).
89
const loaded = discoveryInfo.files
90
.filter(f => f.status === 'loaded')
91
.map(f => f.promptPath.name ?? f.promptPath.uri.path.split('/').pop() ?? f.promptPath.uri.toString());
92
const skipped = discoveryInfo.files.filter(f => f.status === 'skipped').map(f => {
93
const label = f.promptPath.uri.toString();
94
return f.skipReason ? `${label} (${f.skipReason})` : label;
95
});
96
const folders = discoveryInfo.sourceFolders?.map(sf => sf.uri.path) ?? [];
97
const parts: string[] = [];
98
if (details) {
99
parts.push(details);
100
}
101
if (loaded.length > 0) {
102
parts.push(`loaded: [${truncateList(loaded)}]`);
103
}
104
if (skipped.length > 0) {
105
parts.push(`skipped: [${truncateList(skipped)}]`);
106
}
107
if (folders.length > 0) {
108
parts.push(`folders: [${truncateList(folders)}]`);
109
}
110
const newDetails = parts.join(' | ') || undefined;
111
112
chatDebugService.log(
113
sessionResource,
114
name,
115
newDetails,
116
undefined,
117
{ id: eventId, category: 'discovery' },
118
);
119
}
120
} catch (error) {
121
logService.error('Error while logging prompt discovery info to chat debug service', error);
122
} finally {
123
cts.dispose();
124
}
125
}
126
127
// Log resolved customizations from the last instructions collection.
128
const lastResult = lastInstructionsCollectionResult;
129
if (!isFirstInvocation && lastResult) {
130
const { telemetryEvent: collectionEvent, debugInfo } = lastResult;
131
// Fetch the cached hook discovery info.
132
let resolvedHooks: ChatRequestHooks | undefined;
133
try {
134
const hookDiscoveryInfo = await this.promptsService.getDiscoveryInfo(PromptsType.hook, CancellationToken.None) as IHookDiscoveryInfo;
135
resolvedHooks = hookDiscoveryInfo.hooksInfo?.hooks;
136
} catch (error) {
137
logService.warn('Error while fetching hooks for customization debug event', error);
138
}
139
140
const parts: string[] = [];
141
if (collectionEvent.applyingInstructionsCount > 0) {
142
parts.push(localize('customizations.applying', '{0} applying', collectionEvent.applyingInstructionsCount));
143
}
144
if (collectionEvent.referencedInstructionsCount > 0) {
145
parts.push(localize('customizations.referenced', '{0} referenced', collectionEvent.referencedInstructionsCount));
146
}
147
if (collectionEvent.agentInstructionsCount > 0) {
148
parts.push(localize('customizations.agent', '{0} agent', collectionEvent.agentInstructionsCount));
149
}
150
if (collectionEvent.listedInstructionsCount > 0) {
151
parts.push(localize('customizations.listed', '{0} listed', collectionEvent.listedInstructionsCount));
152
}
153
const durationStr = debugInfo.durationInMillis.toFixed(1);
154
const summary = parts.length > 0
155
? localize('customizationsResolved.details', 'Resolved {0} customizations ({1}) in {2}ms', collectionEvent.totalInstructionsCount, parts.join(', '), durationStr)
156
: localize('customizationsResolved.none', 'No customizations resolved');
157
const detailSummaries = debugInfo.debugDetails.map(e => {
158
const detail = e.reason ? `${e.name} — ${e.reason}` : e.name;
159
return `[${e.category}] ${detail}`;
160
});
161
const details = detailSummaries.length > 0
162
? `${summary} | ${detailSummaries.join(', ')}`
163
: summary;
164
165
const customizationEventId = generateUuid();
166
this._customizationEventDetails.set(customizationEventId, { debugInfo, hooks: resolvedHooks });
167
168
// Evict oldest entries when the map exceeds the cap.
169
if (this._customizationEventDetails.size > PromptsDebugContribution.MAX_DISCOVERY_DETAILS) {
170
const first = this._customizationEventDetails.keys().next().value;
171
if (first !== undefined) {
172
this._customizationEventDetails.delete(first);
173
}
174
}
175
176
chatDebugService.log(
177
sessionResource,
178
localize('customizationsResolved', 'Resolve Customizations'),
179
details,
180
undefined,
181
{ id: customizationEventId, category: 'customization' },
182
);
183
}
184
}));
185
// Register a resolve provider so expanding a discovery event
186
// in the Agent Debug Logs shows the full file list.
187
this._register(chatDebugService.registerProvider({
188
provideChatDebugLog: async () => undefined,
189
resolveChatDebugLogEvent: async (eventId) => {
190
return this._resolveDiscoveryEvent(eventId) ?? this._resolveCustomizationEvent(eventId);
191
}
192
}));
193
}
194
195
private getDiscoveryLogEntry(discoveryInfo: IPromptDiscoveryInfo): { readonly name: string; readonly details?: string } {
196
197
const durationInMillis = discoveryInfo.durationInMillis.toFixed(1);
198
const loadedCount = discoveryInfo.files.filter(file => file.status === 'loaded').length;
199
const skippedCount = discoveryInfo.files.length - loadedCount;
200
201
switch (discoveryInfo.type) {
202
case PromptsType.prompt:
203
return {
204
name: localize('promptsService.loadSlashCommands', 'Slash Commands Discovery'),
205
details: loadedCount === 1
206
? localize('promptsDebugContribution.resolvedSlashCommand', 'Resolved {0} slash command in {1}ms', loadedCount, durationInMillis)
207
: localize('promptsDebugContribution.resolvedSlashCommands', 'Resolved {0} slash commands in {1}ms', loadedCount, durationInMillis)
208
};
209
case PromptsType.agent:
210
return {
211
name: localize('promptsService.loadAgents', 'Agent Discovery'),
212
details: loadedCount === 1
213
? localize('promptsDebugContribution.resolvedAgent', 'Resolved {0} agent in {1}ms', loadedCount, durationInMillis)
214
: localize('promptsDebugContribution.resolvedAgents', 'Resolved {0} agents in {1}ms', loadedCount, durationInMillis)
215
};
216
case PromptsType.skill:
217
return {
218
name: localize('promptsService.loadSkills', 'Skill Discovery'),
219
details: loadedCount === 1
220
? localize('promptsDebugContribution.resolvedSkill', 'Resolved {0} skill in {1}ms', loadedCount, durationInMillis)
221
: localize('promptsDebugContribution.resolvedSkills', 'Resolved {0} skills in {1}ms', loadedCount, durationInMillis)
222
};
223
case PromptsType.instructions:
224
return {
225
name: localize('promptsService.loadInstructions', 'Instructions Discovery'),
226
details: loadedCount === 1
227
? localize('promptsDebugContribution.resolvedInstruction', 'Resolved {0} instruction in {1}ms', loadedCount, durationInMillis)
228
: localize('promptsDebugContribution.resolvedInstructions', 'Resolved {0} instructions in {1}ms', loadedCount, durationInMillis)
229
};
230
case PromptsType.hook: {
231
const hookDiscoveryInfo = discoveryInfo as IHookDiscoveryInfo;
232
const hookCount = hookDiscoveryInfo.hooksInfo
233
? Object.values(hookDiscoveryInfo.hooksInfo.hooks).reduce((total, hooks) => total + hooks.length, 0)
234
: loadedCount;
235
const details = skippedCount > 0
236
? localize('promptsDebugContribution.resolvedHooksWithSkipped', 'Resolved {0} hooks from {1} files in {2}ms, skipped {3}', hookCount, loadedCount, durationInMillis, skippedCount)
237
: hookCount === 1
238
? localize('promptsDebugContribution.resolvedHook', 'Resolved {0} hook in {1}ms', hookCount, durationInMillis)
239
: localize('promptsDebugContribution.resolvedHooks', 'Resolved {0} hooks in {1}ms', hookCount, durationInMillis);
240
return {
241
name: localize('promptsService.loadHooks', 'Hook Discovery'),
242
details
243
};
244
}
245
}
246
}
247
248
private _resolveDiscoveryEvent(eventId: string): IChatDebugResolvedEventContent | undefined {
249
const info = this._discoveryEventDetails.get(eventId);
250
if (!info) {
251
return undefined;
252
}
253
254
return this._toFileListContent(info);
255
}
256
257
private _resolveCustomizationEvent(eventId: string): IChatDebugResolvedEventContent | undefined {
258
const data = this._customizationEventDetails.get(eventId);
259
if (!data) {
260
return undefined;
261
}
262
263
const { debugInfo, hooks } = data;
264
const logs: IChatDebugCustomizationLogEntry[] = [...debugInfo.debugDetails];
265
266
// Add hook entries from the resolved hooks — each command carries its sourceUri.
267
if (hooks) {
268
for (const hookType of Object.values(HookType)) {
269
const commands = hooks[hookType];
270
if (commands && commands.length > 0) {
271
for (const cmd of commands) {
272
const commandLabel = formatHookCommandLabel(cmd, OS) || localize('hook.unknownCommand', '(unknown command)');
273
logs.push({
274
category: 'hook',
275
name: commandLabel,
276
reason: hookType,
277
uri: cmd.sourceUri,
278
});
279
}
280
}
281
}
282
}
283
284
return {
285
kind: 'customizationSummary',
286
resolutionLogs: logs,
287
durationInMillis: debugInfo.durationInMillis,
288
counts: {
289
instructions: logs.filter(e => e.category === 'applying' || e.category === 'referenced').length,
290
skills: logs.filter(e => e.category === 'skill').length,
291
agents: logs.filter(e => e.category === 'custom-agent').length,
292
hooks: logs.filter(e => e.category === 'hook').length,
293
skipped: logs.filter(e => e.category === 'skipped').length,
294
},
295
};
296
}
297
298
private _toFileListContent(info: IPromptDiscoveryInfo): IChatDebugEventFileListContent {
299
return {
300
kind: 'fileList',
301
discoveryType: info.type,
302
durationInMillis: info.durationInMillis,
303
files: info.files.map(f => ({
304
uri: f.promptPath.uri,
305
name: f.promptPath.name,
306
status: f.status,
307
storage: f.promptPath.storage,
308
extensionId: f.promptPath.extension?.identifier.value,
309
skipReason: f.skipReason,
310
errorMessage: f.errorMessage,
311
duplicateOf: f.duplicateOf,
312
})),
313
sourceFolders: info.sourceFolders?.map(sf => ({
314
uri: sf.uri,
315
storage: sf.storage,
316
})),
317
};
318
}
319
}
320
321
const MAX_LIST_ITEMS = 100;
322
323
/**
324
* Join a list of strings, truncating after {@link MAX_LIST_ITEMS} entries.
325
* Full details are available via {@link IChatDebugService.resolveEvent}.
326
*/
327
function truncateList(items: string[]): string {
328
if (items.length <= MAX_LIST_ITEMS) {
329
return items.join(', ');
330
}
331
332
return items.slice(0, MAX_LIST_ITEMS).join(', ') + ` (+${items.length - MAX_LIST_ITEMS} more)`;
333
}
334
335