Skip to content

Commit fdaa8c8

Browse files
authored
Move chat debug logging out of PromptsService (#307142)
* Move chat debug logging out of PromptsService * remove sessionResource arg from PromptsService getters * add IChatAgentService.onWillInvokeAgent * remove async * update * .toFixed(1)
1 parent e9dbff5 commit fdaa8c8

16 files changed

Lines changed: 300 additions & 311 deletions

File tree

src/vs/sessions/contrib/chat/browser/promptsService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ export class AgenticPromptsService extends PromptsService {
122122
* Override to include built-in skills, appending them with lowest priority.
123123
* Skills from any other source (workspace, user, extension, internal) take precedence.
124124
*/
125-
public override async findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise<IAgentSkill[] | undefined> {
126-
const baseResult = await super.findAgentSkills(token, sessionResource);
125+
public override async findAgentSkills(token: CancellationToken): Promise<IAgentSkill[] | undefined> {
126+
const baseResult = await super.findAgentSkills(token);
127127
if (baseResult === undefined) {
128128
return undefined;
129129
}

src/vs/workbench/contrib/chat/browser/promptsDebugContribution.ts

Lines changed: 121 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
67
import { Disposable } from '../../../../base/common/lifecycle.js';
78
import { generateUuid } from '../../../../base/common/uuid.js';
9+
import { localize } from '../../../../nls.js';
10+
import { ILogService } from '../../../../platform/log/common/log.js';
811
import { IWorkbenchContribution } from '../../../common/contributions.js';
912
import { IChatDebugResolvedEventContent, IChatDebugService } from '../common/chatDebugService.js';
10-
import { IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
13+
import { IChatAgentService } from '../common/participants/chatAgents.js';
14+
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
15+
import { IHookDiscoveryInfo, IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
1116

1217
/**
13-
* Bridges {@link IPromptsService} discovery log events to {@link IChatDebugService}.
14-
*
15-
* This contribution listens for discovery events emitted by the prompts service
16-
* and forwards them as debug log entries. It also registers a resolve provider
17-
* so expanding a discovery event in the Agent Debug Logs shows the full file list.
18+
* Bridges prompt discovery information to {@link IChatDebugService}.
1819
*/
1920
export class PromptsDebugContribution extends Disposable implements IWorkbenchContribution {
2021

@@ -29,59 +30,74 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
2930
private readonly _discoveryEventDetails = new Map<string, IPromptDiscoveryInfo>();
3031

3132
constructor(
32-
@IPromptsService promptsService: IPromptsService,
33+
@IPromptsService private readonly promptsService: IPromptsService,
34+
@IChatAgentService chatAgentService: IChatAgentService,
3335
@IChatDebugService chatDebugService: IChatDebugService,
36+
@ILogService logService: ILogService,
3437
) {
3538
super();
3639

3740
// Forward discovery log events to the debug service.
38-
this._register(promptsService.onDidLogDiscovery(entry => {
39-
let eventId: string | undefined;
40-
41-
if (entry.discoveryInfo) {
42-
eventId = generateUuid();
43-
this._discoveryEventDetails.set(eventId, entry.discoveryInfo);
44-
45-
// Evict oldest entries when the map exceeds the cap.
46-
if (this._discoveryEventDetails.size > PromptsDebugContribution.MAX_DISCOVERY_DETAILS) {
47-
const first = this._discoveryEventDetails.keys().next().value;
48-
if (first !== undefined) {
49-
this._discoveryEventDetails.delete(first);
41+
this._register(chatAgentService.onWillInvokeAgent(async e => {
42+
const sessionResource = e.request.sessionResource;
43+
const cts = new CancellationTokenSource();
44+
45+
try {
46+
const discoveryInfos = await Promise.all([PromptsType.agent, PromptsType.instructions, PromptsType.prompt, PromptsType.skill, PromptsType.hook].map(type => this.promptsService.getDiscoveryInfo(type, cts.token)));
47+
for (const discoveryInfo of discoveryInfos) {
48+
const { name, details } = this.getDiscoveryLogEntry(discoveryInfo);
49+
const eventId = generateUuid();
50+
51+
this._discoveryEventDetails.set(eventId, discoveryInfo);
52+
53+
// Evict oldest entries when the map exceeds the cap.
54+
if (this._discoveryEventDetails.size > PromptsDebugContribution.MAX_DISCOVERY_DETAILS) {
55+
const first = this._discoveryEventDetails.keys().next().value;
56+
if (first !== undefined) {
57+
this._discoveryEventDetails.delete(first);
58+
}
5059
}
51-
}
52-
}
5360

54-
// Enrich details with file paths so they appear in the event
55-
// payload (e.g. forwarded via onDidReceiveChatDebugEvent to the
56-
// extension's JSONL file logger).
57-
let details = entry.details;
58-
if (entry.discoveryInfo) {
59-
const info = entry.discoveryInfo;
60-
const loaded = info.files
61-
.filter(f => f.status === 'loaded')
62-
.map(f => f.promptPath.name ?? f.promptPath.uri.path.split('/').pop() ?? f.promptPath.uri.toString());
63-
const skipped = info.files.filter(f => f.status === 'skipped').map(f => {
64-
const label = f.promptPath.uri.toString();
65-
return f.skipReason ? `${label} (${f.skipReason})` : label;
66-
});
67-
const folders = info.sourceFolders?.map(sf => sf.uri.path) ?? [];
68-
const parts: string[] = [];
69-
if (details) { parts.push(details); }
70-
if (loaded.length > 0) { parts.push(`loaded: [${truncateList(loaded)}]`); }
71-
if (skipped.length > 0) { parts.push(`skipped: [${truncateList(skipped)}]`); }
72-
if (folders.length > 0) { parts.push(`folders: [${truncateList(folders)}]`); }
73-
details = parts.join(' | ') || undefined;
61+
// Enrich details with file paths so they appear in the event
62+
// payload (e.g. forwarded via onDidReceiveChatDebugEvent to the
63+
// extension's JSONL file logger).
64+
const loaded = discoveryInfo.files
65+
.filter(f => f.status === 'loaded')
66+
.map(f => f.promptPath.name ?? f.promptPath.uri.path.split('/').pop() ?? f.promptPath.uri.toString());
67+
const skipped = discoveryInfo.files.filter(f => f.status === 'skipped').map(f => {
68+
const label = f.promptPath.uri.toString();
69+
return f.skipReason ? `${label} (${f.skipReason})` : label;
70+
});
71+
const folders = discoveryInfo.sourceFolders?.map(sf => sf.uri.path) ?? [];
72+
const parts: string[] = [];
73+
if (details) {
74+
parts.push(details);
75+
}
76+
if (loaded.length > 0) {
77+
parts.push(`loaded: [${truncateList(loaded)}]`);
78+
}
79+
if (skipped.length > 0) {
80+
parts.push(`skipped: [${truncateList(skipped)}]`);
81+
}
82+
if (folders.length > 0) {
83+
parts.push(`folders: [${truncateList(folders)}]`);
84+
}
85+
const newDetails = parts.join(' | ') || undefined;
86+
87+
chatDebugService.log(
88+
sessionResource,
89+
name,
90+
newDetails,
91+
undefined,
92+
{ id: eventId, category: 'discovery' },
93+
);
94+
}
95+
} catch (error) {
96+
logService.error('Error while logging prompt discovery info to chat debug service', error);
97+
} finally {
98+
cts.dispose();
7499
}
75-
76-
chatDebugService.log(
77-
entry.sessionResource,
78-
entry.name,
79-
details,
80-
undefined,
81-
{ id: eventId, category: entry.category },
82-
);
83100
}));
84-
85101
// Register a resolve provider so expanding a discovery event
86102
// in the Agent Debug Logs shows the full file list.
87103
this._register(chatDebugService.registerProvider({
@@ -92,6 +108,59 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
92108
}));
93109
}
94110

111+
private getDiscoveryLogEntry(discoveryInfo: IPromptDiscoveryInfo): { readonly name: string; readonly details?: string } {
112+
113+
const durationInMillis = discoveryInfo.durationInMillis.toFixed(1);
114+
const loadedCount = discoveryInfo.files.filter(file => file.status === 'loaded').length;
115+
const skippedCount = discoveryInfo.files.length - loadedCount;
116+
117+
switch (discoveryInfo.type) {
118+
case PromptsType.prompt:
119+
return {
120+
name: localize('promptsService.loadSlashCommands', 'Load Slash Commands'),
121+
details: loadedCount === 1
122+
? localize('promptsDebugContribution.resolvedSlashCommand', 'Resolved {0} slash command in {1}ms', loadedCount, durationInMillis)
123+
: localize('promptsDebugContribution.resolvedSlashCommands', 'Resolved {0} slash commands in {1}ms', loadedCount, durationInMillis)
124+
};
125+
case PromptsType.agent:
126+
return {
127+
name: localize('promptsService.loadAgents', 'Load Agents'),
128+
details: loadedCount === 1
129+
? localize('promptsDebugContribution.resolvedAgent', 'Resolved {0} agent in {1}ms', loadedCount, durationInMillis)
130+
: localize('promptsDebugContribution.resolvedAgents', 'Resolved {0} agents in {1}ms', loadedCount, durationInMillis)
131+
};
132+
case PromptsType.skill:
133+
return {
134+
name: localize('promptsService.loadSkills', 'Load Skills'),
135+
details: loadedCount === 1
136+
? localize('promptsDebugContribution.resolvedSkill', 'Resolved {0} skill in {1}ms', loadedCount, durationInMillis)
137+
: localize('promptsDebugContribution.resolvedSkills', 'Resolved {0} skills in {1}ms', loadedCount, durationInMillis)
138+
};
139+
case PromptsType.instructions:
140+
return {
141+
name: localize('promptsService.loadInstructions', 'Load Instructions'),
142+
details: loadedCount === 1
143+
? localize('promptsDebugContribution.resolvedInstruction', 'Resolved {0} instruction in {1}ms', loadedCount, durationInMillis)
144+
: localize('promptsDebugContribution.resolvedInstructions', 'Resolved {0} instructions in {1}ms', loadedCount, durationInMillis)
145+
};
146+
case PromptsType.hook: {
147+
const hookDiscoveryInfo = discoveryInfo as IHookDiscoveryInfo;
148+
const hookCount = hookDiscoveryInfo.hooksInfo
149+
? Object.values(hookDiscoveryInfo.hooksInfo.hooks).reduce((total, hooks) => total + hooks.length, 0)
150+
: loadedCount;
151+
const details = skippedCount > 0
152+
? localize('promptsDebugContribution.resolvedHooksWithSkipped', 'Resolved {0} hooks from {1} files in {2}ms, skipped {3}', hookCount, loadedCount, durationInMillis, skippedCount)
153+
: hookCount === 1
154+
? localize('promptsDebugContribution.resolvedHook', 'Resolved {0} hook in {1}ms', hookCount, durationInMillis)
155+
: localize('promptsDebugContribution.resolvedHooks', 'Resolved {0} hooks in {1}ms', hookCount, durationInMillis);
156+
return {
157+
name: localize('promptsService.loadHooks', 'Load Hooks'),
158+
details
159+
};
160+
}
161+
}
162+
}
163+
95164
private _resolveDiscoveryEvent(eventId: string): IChatDebugResolvedEventContent | undefined {
96165
const info = this._discoveryEventDetails.get(eventId);
97166
if (!info) {
@@ -101,6 +170,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
101170
return {
102171
kind: 'fileList',
103172
discoveryType: info.type,
173+
durationInMillis: info.durationInMillis,
104174
files: info.files.map(f => ({
105175
uri: f.promptPath.uri,
106176
name: f.promptPath.name,
@@ -129,5 +199,6 @@ function truncateList(items: string[]): string {
129199
if (items.length <= MAX_LIST_ITEMS) {
130200
return items.join(', ');
131201
}
202+
132203
return items.slice(0, MAX_LIST_ITEMS).join(', ') + ` (+${items.length - MAX_LIST_ITEMS} more)`;
133204
}

src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2817,8 +2817,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
28172817
this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are enabled`);
28182818
const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.userSelectedTools.get() : undefined;
28192819
const enabledSubAgents = this.input.currentModeKind === ChatModeKind.Agent ? this.input.currentModeObs.get().agents?.get() : undefined;
2820-
const sessionResource = this._viewModel?.model.sessionResource;
2821-
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeKind, enabledTools, enabledSubAgents, sessionResource);
2820+
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeKind, enabledTools, enabledSubAgents);
28222821
await computer.collect(attachedContext, CancellationToken.None);
28232822
} catch (err) {
28242823
this.logService.error(`ChatWidget#_autoAttachInstructions: failed to compute automatic instructions`, err);

src/vs/workbench/contrib/chat/common/chatDebugService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ export interface IChatDebugSourceFolderEntry {
294294
export interface IChatDebugEventFileListContent {
295295
readonly kind: 'fileList';
296296
readonly discoveryType: string;
297+
readonly durationInMillis: number;
297298
readonly files: readonly IChatDebugFileEntry[];
298299
readonly sourceFolders?: readonly IChatDebugSourceFolderEntry[];
299300
}

src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,7 @@ export class ChatService extends Disposable implements IChatService {
11141114
let collectedHooks: ChatRequestHooks | undefined;
11151115
let hasDisabledClaudeHooks = false;
11161116
try {
1117-
const hooksInfo = await this.promptsService.getHooks(token, model.sessionResource);
1117+
const hooksInfo = await this.promptsService.getHooks(token);
11181118
if (hooksInfo) {
11191119
collectedHooks = hooksInfo.hooks;
11201120
hasDisabledClaudeHooks = hooksInfo.hasDisabledClaudeHooks;
@@ -1127,7 +1127,7 @@ export class ChatService extends Disposable implements IChatService {
11271127
const agentName = options?.modeInfo?.modeInstructions?.name;
11281128
if (agentName) {
11291129
try {
1130-
const agents = await this.promptsService.getCustomAgents(token, model.sessionResource);
1130+
const agents = await this.promptsService.getCustomAgents(token);
11311131
const customAgent = agents.find(a => a.name === agentName);
11321132
if (customAgent?.hooks) {
11331133
collectedHooks = mergeHooks(collectedHooks, customAgent.hooks);

src/vs/workbench/contrib/chat/common/participants/chatAgents.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,18 @@ export interface IChatAgentCompletionItem {
218218
command?: Command;
219219
}
220220

221+
export interface IChatAgentInvocationEvent {
222+
readonly agentId: string;
223+
readonly request: Readonly<IChatAgentRequest>;
224+
}
225+
221226
export interface IChatAgentService {
222227
_serviceBrand: undefined;
223228
/**
224229
* undefined when an agent was removed
225230
*/
226231
readonly onDidChangeAgents: Event<IChatAgent | undefined>;
232+
readonly onWillInvokeAgent: Event<IChatAgentInvocationEvent>;
227233
readonly hasToolsAgent: boolean;
228234
registerAgent(id: string, data: IChatAgentData): IDisposable;
229235
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable;
@@ -268,6 +274,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
268274

269275
private readonly _onDidChangeAgents = this._register(new Emitter<IChatAgent | undefined>());
270276
readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event;
277+
private readonly _onWillInvokeAgent = this._register(new Emitter<IChatAgentInvocationEvent>());
278+
readonly onWillInvokeAgent: Event<IChatAgentInvocationEvent> = this._onWillInvokeAgent.event;
271279

272280
private readonly _agentsContextKeys = new Set<string>();
273281
private readonly _hasDefaultAgent: IContextKey<boolean>;
@@ -514,6 +522,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
514522
throw new Error(`No activated agent with id "${id}"`);
515523
}
516524

525+
this._onWillInvokeAgent.fire({ agentId: id, request });
517526
const result = await data.impl.invoke(request, progress, history, token);
518527
markChat(request.sessionResource, ChatPerfMark.AgentDidInvoke);
519528
return result;

src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export class ComputeAutomaticInstructions {
6767
private readonly _modeKind: ChatModeKind,
6868
private readonly _enabledTools: UserSelectedTools | undefined,
6969
private readonly _enabledSubagents: (readonly string[]) | undefined,
70-
private readonly _sessionResource: URI | undefined,
7170
@IPromptsService private readonly _promptsService: IPromptsService,
7271
@ILogService public readonly _logService: ILogService,
7372
@ILabelService private readonly _labelService: ILabelService,
@@ -99,7 +98,7 @@ export class ComputeAutomaticInstructions {
9998

10099
public async collect(variables: ChatRequestVariableSet, token: CancellationToken): Promise<void> {
101100

102-
const instructionFiles = await this._promptsService.getInstructionFiles(token, this._sessionResource);
101+
const instructionFiles = await this._promptsService.getInstructionFiles(token);
103102

104103
this._logService.trace(`[InstructionsContextComputer] ${instructionFiles.length} instruction files available.`);
105104

@@ -394,7 +393,7 @@ export class ComputeAutomaticInstructions {
394393
entries.push('</instructions>', '', ''); // add trailing newline
395394
}
396395

397-
const agentSkills = await this._promptsService.findAgentSkills(token, this._sessionResource);
396+
const agentSkills = await this._promptsService.findAgentSkills(token);
398397
// Filter out skills with disableModelInvocation=true (they can only be triggered manually via /name)
399398
// Also filter by `when` clause using the scoped context key service
400399
// Also filter out the troubleshoot skill when the feature flags are disabled
@@ -458,7 +457,7 @@ export class ComputeAutomaticInstructions {
458457
return (agent: ICustomAgent) => subagents.includes(agent.name);
459458
}
460459
})();
461-
const agents = await this._promptsService.getCustomAgents(token, this._sessionResource);
460+
const agents = await this._promptsService.getCustomAgents(token);
462461
if (agents.length > 0) {
463462
entries.push('<agents>');
464463
entries.push('Here is a list of agents that can be used when running a subagent.');

0 commit comments

Comments
 (0)