Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostChatDebug.ts
13397 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 type * as vscode from 'vscode';
7
import { VSBuffer } from '../../../base/common/buffer.js';
8
import { CancellationToken } from '../../../base/common/cancellation.js';
9
import { Emitter } from '../../../base/common/event.js';
10
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
11
import { URI, UriComponents } from '../../../base/common/uri.js';
12
import { ExtHostChatDebugShape, IChatDebugEventDto, IChatDebugResolvedEventContentDto, MainContext, MainThreadChatDebugShape } from './extHost.protocol.js';
13
import { ChatDebugGenericEvent, ChatDebugHookResult, ChatDebugLogLevel, ChatDebugMessageContentType, ChatDebugMessageSection, ChatDebugModelTurnEvent, ChatDebugSubagentInvocationEvent, ChatDebugSubagentStatus, ChatDebugToolCallEvent, ChatDebugToolCallResult, ChatDebugUserMessageEvent, ChatDebugAgentResponseEvent, ChatDebugEventHookContent } from './extHostTypes.js';
14
import { IExtHostRpcService } from './extHostRpcService.js';
15
16
export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShape {
17
declare _serviceBrand: undefined;
18
19
private readonly _proxy: MainThreadChatDebugShape;
20
private _provider: vscode.ChatDebugLogProvider | undefined;
21
private _nextHandle: number = 0;
22
/** Progress pipelines keyed by `${handle}:${sessionResource}` so multiple sessions can stream concurrently. */
23
private readonly _activeProgress = new Map<string, DisposableStore>();
24
25
private readonly _onDidAddCoreEvent = this._register(new Emitter<vscode.ChatDebugEvent>({
26
onWillAddFirstListener: () => this._proxy.$subscribeToCoreDebugEvents(),
27
onDidRemoveLastListener: () => this._proxy.$unsubscribeFromCoreDebugEvents(),
28
}));
29
readonly onDidAddCoreEvent = this._onDidAddCoreEvent.event;
30
31
constructor(
32
@IExtHostRpcService extHostRpc: IExtHostRpcService,
33
) {
34
super();
35
this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatDebug);
36
}
37
38
private _progressKey(handle: number, sessionResource: UriComponents): string {
39
return `${handle}:${URI.revive(sessionResource).toString()}`;
40
}
41
42
private _cleanupProgress(key: string): void {
43
const store = this._activeProgress.get(key);
44
if (store) {
45
store.dispose();
46
this._activeProgress.delete(key);
47
}
48
}
49
50
registerChatDebugLogProvider(provider: vscode.ChatDebugLogProvider): vscode.Disposable {
51
if (this._provider) {
52
throw new Error('A ChatDebugLogProvider is already registered.');
53
}
54
this._provider = provider;
55
const handle = this._nextHandle++;
56
this._proxy.$registerChatDebugLogProvider(handle);
57
58
return toDisposable(() => {
59
this._provider = undefined;
60
// Clean up all progress pipelines for this handle
61
for (const [key, store] of this._activeProgress) {
62
if (key.startsWith(`${handle}:`)) {
63
store.dispose();
64
this._activeProgress.delete(key);
65
}
66
}
67
this._proxy.$unregisterChatDebugLogProvider(handle);
68
});
69
}
70
71
async $provideChatDebugLog(handle: number, sessionResource: UriComponents, token: CancellationToken): Promise<IChatDebugEventDto[] | undefined> {
72
if (!this._provider) {
73
return undefined;
74
}
75
76
// Clean up any previous progress pipeline for this handle+session pair
77
const key = this._progressKey(handle, sessionResource);
78
this._cleanupProgress(key);
79
80
const store = new DisposableStore();
81
this._activeProgress.set(key, store);
82
83
const emitter = store.add(new Emitter<vscode.ChatDebugEvent>());
84
85
// Forward progress events to the main thread
86
store.add(emitter.event(event => {
87
const dto = this._serializeEvent(event);
88
if (!dto.sessionResource) {
89
(dto as { sessionResource?: UriComponents }).sessionResource = sessionResource;
90
}
91
this._proxy.$acceptChatDebugEvent(handle, dto);
92
}));
93
94
// Clean up when the token is cancelled
95
store.add(token.onCancellationRequested(() => {
96
this._cleanupProgress(key);
97
}));
98
99
try {
100
const progress: vscode.Progress<vscode.ChatDebugEvent> = {
101
report: (value) => emitter.fire(value)
102
};
103
104
const sessionUri = URI.revive(sessionResource);
105
const result = await this._provider.provideChatDebugLog(sessionUri, progress, token);
106
if (!result) {
107
return undefined;
108
}
109
110
return result.map(event => this._serializeEvent(event));
111
} catch (err) {
112
this._cleanupProgress(key);
113
throw err;
114
}
115
// Note: do NOT dispose progress pipeline here - keep it alive for
116
// streaming events via progress.report() after the initial return.
117
// It will be cleaned up when a new session is requested, the token
118
// is cancelled, or the provider is unregistered.
119
}
120
121
private _serializeEvent(event: vscode.ChatDebugEvent): IChatDebugEventDto {
122
const base = {
123
id: event.id,
124
sessionResource: (event as { sessionResource?: vscode.Uri }).sessionResource,
125
created: event.created.getTime(),
126
parentEventId: event.parentEventId,
127
};
128
129
// Use the _kind discriminant set by all event class constructors.
130
// This works both for direct instances and when extensions bundle
131
// their own copy of the API types (where instanceof would fail).
132
const kind = (event as { _kind?: string })._kind;
133
switch (kind) {
134
case 'toolCall': {
135
const e = event as vscode.ChatDebugToolCallEvent;
136
return {
137
...base,
138
kind: 'toolCall',
139
toolName: e.toolName,
140
toolCallId: e.toolCallId,
141
input: e.input,
142
output: e.output,
143
result: e.result === ChatDebugToolCallResult.Success ? 'success'
144
: e.result === ChatDebugToolCallResult.Error ? 'error'
145
: undefined,
146
durationInMillis: e.durationInMillis,
147
};
148
}
149
case 'modelTurn': {
150
const e = event as vscode.ChatDebugModelTurnEvent;
151
return {
152
...base,
153
kind: 'modelTurn',
154
model: e.model,
155
requestName: e.requestName,
156
inputTokens: e.inputTokens,
157
outputTokens: e.outputTokens,
158
cachedTokens: e.cachedTokens,
159
totalTokens: e.totalTokens,
160
durationInMillis: e.durationInMillis,
161
};
162
}
163
case 'generic': {
164
const e = event as vscode.ChatDebugGenericEvent;
165
return {
166
...base,
167
kind: 'generic',
168
name: e.name,
169
details: e.details,
170
level: e.level,
171
category: e.category,
172
};
173
}
174
case 'subagentInvocation': {
175
const e = event as vscode.ChatDebugSubagentInvocationEvent;
176
return {
177
...base,
178
kind: 'subagentInvocation',
179
agentName: e.agentName,
180
description: e.description,
181
status: e.status === ChatDebugSubagentStatus.Running ? 'running'
182
: e.status === ChatDebugSubagentStatus.Completed ? 'completed'
183
: e.status === ChatDebugSubagentStatus.Failed ? 'failed'
184
: undefined,
185
durationInMillis: e.durationInMillis,
186
toolCallCount: e.toolCallCount,
187
modelTurnCount: e.modelTurnCount,
188
};
189
}
190
case 'userMessage': {
191
const e = event as vscode.ChatDebugUserMessageEvent;
192
return {
193
...base,
194
kind: 'userMessage',
195
message: e.message,
196
sections: e.sections.map(s => ({ name: s.name, content: s.content })),
197
};
198
}
199
case 'agentResponse': {
200
const e = event as vscode.ChatDebugAgentResponseEvent;
201
return {
202
...base,
203
kind: 'agentResponse',
204
message: e.message,
205
sections: e.sections.map(s => ({ name: s.name, content: s.content })),
206
};
207
}
208
default: {
209
const generic = event as vscode.ChatDebugGenericEvent;
210
const rawName = generic.name;
211
const rawDetails = generic.details;
212
return {
213
...base,
214
kind: 'generic',
215
name: typeof rawName === 'string' ? rawName : '',
216
details: typeof rawDetails === 'string' ? rawDetails : undefined,
217
level: generic.level ?? 1,
218
category: generic.category,
219
};
220
}
221
}
222
}
223
224
async $resolveChatDebugLogEvent(_handle: number, eventId: string, token: CancellationToken): Promise<IChatDebugResolvedEventContentDto | undefined> {
225
if (!this._provider?.resolveChatDebugLogEvent) {
226
return undefined;
227
}
228
const result = await this._provider.resolveChatDebugLogEvent(eventId, token);
229
if (!result) {
230
return undefined;
231
}
232
233
// Use the _kind discriminant set by all content class constructors.
234
const kind = (result as { _kind?: string })._kind;
235
switch (kind) {
236
case 'text':
237
return { kind: 'text', value: (result as vscode.ChatDebugEventTextContent).value };
238
case 'messageContent': {
239
const msg = result as vscode.ChatDebugEventMessageContent;
240
return {
241
kind: 'message',
242
type: msg.type === ChatDebugMessageContentType.User ? 'user' : 'agent',
243
message: msg.message,
244
sections: msg.sections.map(s => ({ name: s.name, content: s.content })),
245
};
246
}
247
case 'userMessage': {
248
const msg = result as vscode.ChatDebugUserMessageEvent;
249
return {
250
kind: 'message',
251
type: 'user',
252
message: msg.message,
253
sections: msg.sections.map(s => ({ name: s.name, content: s.content })),
254
};
255
}
256
case 'agentResponse': {
257
const msg = result as vscode.ChatDebugAgentResponseEvent;
258
return {
259
kind: 'message',
260
type: 'agent',
261
message: msg.message,
262
sections: msg.sections.map(s => ({ name: s.name, content: s.content })),
263
};
264
}
265
case 'toolCallContent': {
266
const tc = result as vscode.ChatDebugEventToolCallContent;
267
return {
268
kind: 'toolCall',
269
toolName: tc.toolName,
270
result: tc.result === ChatDebugToolCallResult.Success ? 'success'
271
: tc.result === ChatDebugToolCallResult.Error ? 'error'
272
: undefined,
273
durationInMillis: tc.durationInMillis,
274
input: tc.input,
275
output: tc.output,
276
};
277
}
278
case 'modelTurnContent': {
279
const mt = result as vscode.ChatDebugEventModelTurnContent;
280
return {
281
kind: 'modelTurn',
282
requestName: mt.requestName,
283
model: mt.model,
284
status: mt.status,
285
durationInMillis: mt.durationInMillis,
286
timeToFirstTokenInMillis: mt.timeToFirstTokenInMillis,
287
maxInputTokens: mt.maxInputTokens,
288
maxOutputTokens: mt.maxOutputTokens,
289
inputTokens: mt.inputTokens,
290
outputTokens: mt.outputTokens,
291
cachedTokens: mt.cachedTokens,
292
totalTokens: mt.totalTokens,
293
errorMessage: mt.errorMessage,
294
sections: mt.sections?.map(s => ({ name: s.name, content: s.content })),
295
};
296
}
297
case 'hookContent': {
298
const hk = result as unknown as ChatDebugEventHookContent;
299
return {
300
kind: 'hook',
301
hookType: hk.hookType,
302
command: hk.command,
303
result: hk.result === ChatDebugHookResult.Success ? 'success'
304
: hk.result === ChatDebugHookResult.Error ? 'error'
305
: hk.result === ChatDebugHookResult.NonBlockingError ? 'nonBlockingError'
306
: undefined,
307
durationInMillis: hk.durationInMillis,
308
input: hk.input,
309
output: hk.output,
310
exitCode: hk.exitCode,
311
errorMessage: hk.errorMessage,
312
};
313
}
314
default:
315
return undefined;
316
}
317
}
318
319
private _deserializeEvent(dto: IChatDebugEventDto): vscode.ChatDebugEvent | undefined {
320
const created = new Date(dto.created);
321
const sessionResource = dto.sessionResource ? URI.revive(dto.sessionResource) : undefined;
322
switch (dto.kind) {
323
case 'toolCall': {
324
const evt = new ChatDebugToolCallEvent(dto.toolName, created);
325
evt.id = dto.id;
326
evt.sessionResource = sessionResource;
327
evt.parentEventId = dto.parentEventId;
328
evt.toolCallId = dto.toolCallId;
329
evt.input = dto.input;
330
evt.output = dto.output;
331
evt.result = dto.result === 'success' ? ChatDebugToolCallResult.Success
332
: dto.result === 'error' ? ChatDebugToolCallResult.Error
333
: undefined;
334
evt.durationInMillis = dto.durationInMillis;
335
return evt;
336
}
337
case 'modelTurn': {
338
const evt = new ChatDebugModelTurnEvent(created);
339
evt.id = dto.id;
340
evt.sessionResource = sessionResource;
341
evt.parentEventId = dto.parentEventId;
342
evt.model = dto.model;
343
evt.inputTokens = dto.inputTokens;
344
evt.outputTokens = dto.outputTokens;
345
evt.cachedTokens = dto.cachedTokens;
346
evt.totalTokens = dto.totalTokens;
347
evt.durationInMillis = dto.durationInMillis;
348
return evt;
349
}
350
case 'generic': {
351
const evt = new ChatDebugGenericEvent(dto.name, dto.level as ChatDebugLogLevel, created);
352
evt.id = dto.id;
353
evt.sessionResource = sessionResource;
354
evt.parentEventId = dto.parentEventId;
355
evt.details = dto.details;
356
evt.category = dto.category;
357
return evt;
358
}
359
case 'subagentInvocation': {
360
const evt = new ChatDebugSubagentInvocationEvent(dto.agentName, created);
361
evt.id = dto.id;
362
evt.sessionResource = sessionResource;
363
evt.parentEventId = dto.parentEventId;
364
evt.description = dto.description;
365
evt.status = dto.status === 'running' ? ChatDebugSubagentStatus.Running
366
: dto.status === 'completed' ? ChatDebugSubagentStatus.Completed
367
: dto.status === 'failed' ? ChatDebugSubagentStatus.Failed
368
: undefined;
369
evt.durationInMillis = dto.durationInMillis;
370
evt.toolCallCount = dto.toolCallCount;
371
evt.modelTurnCount = dto.modelTurnCount;
372
return evt;
373
}
374
case 'userMessage': {
375
const evt = new ChatDebugUserMessageEvent(dto.message, created);
376
evt.id = dto.id;
377
evt.sessionResource = sessionResource;
378
evt.parentEventId = dto.parentEventId;
379
evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content));
380
return evt;
381
}
382
case 'agentResponse': {
383
const evt = new ChatDebugAgentResponseEvent(dto.message, created);
384
evt.id = dto.id;
385
evt.sessionResource = sessionResource;
386
evt.parentEventId = dto.parentEventId;
387
evt.sections = dto.sections.map(s => new ChatDebugMessageSection(s.name, s.content));
388
return evt;
389
}
390
default:
391
return undefined;
392
}
393
}
394
395
$onCoreDebugEvent(dto: IChatDebugEventDto): void {
396
const event = this._deserializeEvent(dto);
397
if (event) {
398
this._onDidAddCoreEvent.fire(event);
399
}
400
}
401
402
async $exportChatDebugLog(_handle: number, sessionResource: UriComponents, coreEventDtos: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise<VSBuffer | undefined> {
403
if (!this._provider?.provideChatDebugLogExport) {
404
return undefined;
405
}
406
const sessionUri = URI.revive(sessionResource);
407
const coreEvents = coreEventDtos.map(dto => this._deserializeEvent(dto)).filter((e): e is vscode.ChatDebugEvent => e !== undefined);
408
const options: vscode.ChatDebugLogExportOptions = { coreEvents, sessionTitle };
409
const result = await this._provider.provideChatDebugLogExport(sessionUri, options, token);
410
if (!result) {
411
return undefined;
412
}
413
return VSBuffer.wrap(result);
414
}
415
416
async $importChatDebugLog(_handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined> {
417
if (!this._provider?.resolveChatDebugLogImport) {
418
return undefined;
419
}
420
const result = await this._provider.resolveChatDebugLogImport(data.buffer, token);
421
if (!result) {
422
return undefined;
423
}
424
return { uri: result.uri, sessionTitle: result.sessionTitle };
425
}
426
427
async $getAvailableDebugSessionResources(_handle: number, token: CancellationToken): Promise<{ uri: UriComponents; title?: string }[]> {
428
if (!this._provider?.provideAvailableDebugSessionResources) {
429
return [];
430
}
431
const result = await this._provider.provideAvailableDebugSessionResources(token);
432
return result ?? [];
433
}
434
435
override dispose(): void {
436
for (const store of this._activeProgress.values()) {
437
store.dispose();
438
}
439
this._activeProgress.clear();
440
super.dispose();
441
}
442
}
443
444