Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts
5263 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 { DeferredPromise } from '../../../../../base/common/async.js';
7
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
8
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
9
import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
12
import { Iterable } from '../../../../../base/common/iterator.js';
13
import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
14
import { revive } from '../../../../../base/common/marshalling.js';
15
import { Schemas } from '../../../../../base/common/network.js';
16
import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
17
import { isEqual } from '../../../../../base/common/resources.js';
18
import { StopWatch } from '../../../../../base/common/stopwatch.js';
19
import { isDefined } from '../../../../../base/common/types.js';
20
import { URI } from '../../../../../base/common/uri.js';
21
import { generateUuid } from '../../../../../base/common/uuid.js';
22
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
23
import { localize } from '../../../../../nls.js';
24
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
25
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
26
import { ILogService } from '../../../../../platform/log/common/log.js';
27
import { Progress } from '../../../../../platform/progress/common/progress.js';
28
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
29
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
30
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
31
import { InlineChatConfigKeys } from '../../../inlineChat/common/inlineChat.js';
32
import { IMcpService } from '../../../mcp/common/mcpTypes.js';
33
import { awaitStatsForSession } from '../chat.js';
34
import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../participants/chatAgents.js';
35
import { chatEditingSessionIsReady } from '../editing/chatEditingService.js';
36
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from '../model/chatModel.js';
37
import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js';
38
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js';
39
import { ChatRequestParser } from '../requestParser/chatRequestParser.js';
40
import { ChatMcpServersStarting, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js';
41
import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';
42
import { IChatSessionsService } from '../chatSessionsService.js';
43
import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js';
44
import { IChatSlashCommandService } from '../participants/chatSlashCommands.js';
45
import { IChatTransferService } from '../model/chatTransferService.js';
46
import { LocalChatSessionUri } from '../model/chatUri.js';
47
import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';
48
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js';
49
import { ChatMessageRole, IChatMessage } from '../languageModels.js';
50
import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js';
51
import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js';
52
import { IPromptsService } from '../promptSyntax/service/promptsService.js';
53
import { IChatRequestHooks } from '../promptSyntax/hookSchema.js';
54
55
const serializedChatKey = 'interactive.sessions';
56
57
class CancellableRequest implements IDisposable {
58
private readonly _yieldRequested: ISettableObservable<boolean> = observableValue(this, false);
59
60
get yieldRequested(): IObservable<boolean> {
61
return this._yieldRequested;
62
}
63
64
constructor(
65
public readonly cancellationTokenSource: CancellationTokenSource,
66
public requestId: string | undefined,
67
@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService
68
) { }
69
70
dispose() {
71
this.cancellationTokenSource.dispose();
72
}
73
74
cancel() {
75
if (this.requestId) {
76
this.toolsService.cancelToolCallsForRequest(this.requestId);
77
}
78
79
this.cancellationTokenSource.cancel();
80
}
81
82
setYieldRequested(): void {
83
this._yieldRequested.set(true, undefined);
84
}
85
}
86
87
export class ChatService extends Disposable implements IChatService {
88
declare _serviceBrand: undefined;
89
90
private readonly _sessionModels: ChatModelStore;
91
private readonly _pendingRequests = this._register(new DisposableResourceMap<CancellableRequest>());
92
private readonly _queuedRequestDeferreds = new Map<string, DeferredPromise<ChatSendResult>>();
93
private _saveModelsEnabled = true;
94
95
private _transferredSessionResource: URI | undefined;
96
public get transferredSessionResource(): URI | undefined {
97
return this._transferredSessionResource;
98
}
99
100
private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>());
101
public readonly onDidSubmitRequest = this._onDidSubmitRequest.event;
102
103
public get onDidCreateModel() { return this._sessionModels.onDidCreateModel; }
104
105
private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());
106
public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;
107
108
private readonly _onDidReceiveQuestionCarouselAnswer = this._register(new Emitter<{ requestId: string; resolveId: string; answers: Record<string, unknown> | undefined }>());
109
public readonly onDidReceiveQuestionCarouselAnswer = this._onDidReceiveQuestionCarouselAnswer.event;
110
111
private readonly _onDidDisposeSession = this._register(new Emitter<{ readonly sessionResource: URI[]; reason: 'cleared' }>());
112
public readonly onDidDisposeSession = this._onDidDisposeSession.event;
113
114
private readonly _sessionFollowupCancelTokens = this._register(new DisposableResourceMap<CancellationTokenSource>());
115
private readonly _chatServiceTelemetry: ChatServiceTelemetry;
116
private readonly _chatSessionStore: ChatSessionStore;
117
118
readonly requestInProgressObs: IObservable<boolean>;
119
120
readonly chatModels: IObservable<Iterable<IChatModel>>;
121
122
/**
123
* For test use only
124
*/
125
setSaveModelsEnabled(enabled: boolean): void {
126
this._saveModelsEnabled = enabled;
127
}
128
129
/**
130
* For test use only
131
*/
132
waitForModelDisposals(): Promise<void> {
133
return this._sessionModels.waitForModelDisposals();
134
}
135
136
public get edits2Enabled(): boolean {
137
return this.configurationService.getValue(ChatConfiguration.Edits2Enabled);
138
}
139
140
private get isEmptyWindow(): boolean {
141
const workspace = this.workspaceContextService.getWorkspace();
142
return !workspace.configuration && workspace.folders.length === 0;
143
}
144
145
constructor(
146
@IStorageService private readonly storageService: IStorageService,
147
@ILogService private readonly logService: ILogService,
148
@IExtensionService private readonly extensionService: IExtensionService,
149
@IInstantiationService private readonly instantiationService: IInstantiationService,
150
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
151
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,
152
@IChatAgentService private readonly chatAgentService: IChatAgentService,
153
@IConfigurationService private readonly configurationService: IConfigurationService,
154
@IChatTransferService private readonly chatTransferService: IChatTransferService,
155
@IChatSessionsService private readonly chatSessionService: IChatSessionsService,
156
@IMcpService private readonly mcpService: IMcpService,
157
@IPromptsService private readonly promptsService: IPromptsService,
158
) {
159
super();
160
161
this._sessionModels = this._register(instantiationService.createInstance(ChatModelStore, {
162
createModel: (props: IStartSessionProps) => this._startSession(props),
163
willDisposeModel: async (model: ChatModel) => {
164
const localSessionId = LocalChatSessionUri.parseLocalSessionId(model.sessionResource);
165
if (localSessionId && this.shouldStoreSession(model)) {
166
// Always preserve sessions that have custom titles, even if empty
167
if (model.getRequests().length === 0 && !model.customTitle) {
168
await this._chatSessionStore.deleteSession(localSessionId);
169
} else if (this._saveModelsEnabled) {
170
await this._chatSessionStore.storeSessions([model]);
171
}
172
} else if (!localSessionId && model.getRequests().length > 0) {
173
await this._chatSessionStore.storeSessionsMetadataOnly([model]);
174
}
175
}
176
}));
177
this._register(this._sessionModels.onDidDisposeModel(model => {
178
this._onDidDisposeSession.fire({ sessionResource: [model.sessionResource], reason: 'cleared' });
179
}));
180
181
this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry);
182
this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));
183
this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData());
184
185
const transferredData = this._chatSessionStore.getTransferredSessionData();
186
if (transferredData) {
187
this.trace('constructor', `Transferred session ${transferredData}`);
188
this._transferredSessionResource = transferredData;
189
}
190
191
this.reviveSessionsWithEdits();
192
193
this._register(storageService.onWillSaveState(() => this.saveState()));
194
195
this.chatModels = derived(this, reader => [...this._sessionModels.observable.read(reader).values()]);
196
197
this.requestInProgressObs = derived(reader => {
198
const models = this._sessionModels.observable.read(reader).values();
199
return Iterable.some(models, model => model.requestInProgress.read(reader));
200
});
201
}
202
203
public get editingSessions() {
204
return [...this._sessionModels.values()].map(v => v.editingSession).filter(isDefined);
205
}
206
207
isEnabled(location: ChatAgentLocation): boolean {
208
return this.chatAgentService.getContributedDefaultAgent(location) !== undefined;
209
}
210
211
private migrateData(): ISerializableChatsData | undefined {
212
const sessionData = this.storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');
213
if (sessionData) {
214
const persistedSessions = this.deserializeChats(sessionData);
215
const countsForLog = Object.keys(persistedSessions).length;
216
if (countsForLog > 0) {
217
this.info('migrateData', `Restored ${countsForLog} persisted sessions`);
218
}
219
220
return persistedSessions;
221
}
222
223
return;
224
}
225
226
private saveState(): void {
227
if (!this._saveModelsEnabled) {
228
return;
229
}
230
231
const liveLocalChats = Array.from(this._sessionModels.values())
232
.filter(session => this.shouldStoreSession(session));
233
234
this._chatSessionStore.storeSessions(liveLocalChats);
235
236
const liveNonLocalChats = Array.from(this._sessionModels.values())
237
.filter(session => !LocalChatSessionUri.parseLocalSessionId(session.sessionResource));
238
this._chatSessionStore.storeSessionsMetadataOnly(liveNonLocalChats);
239
}
240
241
/**
242
* Only persist local sessions from chat that are not imported.
243
*/
244
private shouldStoreSession(session: ChatModel): boolean {
245
if (!LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) {
246
return false;
247
}
248
return session.initialLocation === ChatAgentLocation.Chat && !session.isImported;
249
}
250
251
notifyUserAction(action: IChatUserActionEvent): void {
252
this._chatServiceTelemetry.notifyUserAction(action);
253
this._onDidPerformUserAction.fire(action);
254
if (action.action.kind === 'chatEditingSessionAction') {
255
const model = this._sessionModels.get(action.sessionResource);
256
if (model) {
257
model.notifyEditingAction(action.action);
258
}
259
}
260
}
261
262
notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record<string, unknown> | undefined): void {
263
this._onDidReceiveQuestionCarouselAnswer.fire({ requestId, resolveId, answers });
264
}
265
266
async setChatSessionTitle(sessionResource: URI, title: string): Promise<void> {
267
const model = this._sessionModels.get(sessionResource);
268
if (model) {
269
model.setCustomTitle(title);
270
}
271
272
// Update the title in the file storage
273
const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
274
if (localSessionId) {
275
await this._chatSessionStore.setSessionTitle(localSessionId, title);
276
// Trigger immediate save to ensure consistency
277
this.saveState();
278
}
279
}
280
281
private trace(method: string, message?: string): void {
282
if (message) {
283
this.logService.trace(`ChatService#${method}: ${message}`);
284
} else {
285
this.logService.trace(`ChatService#${method}`);
286
}
287
}
288
289
private info(method: string, message?: string): void {
290
if (message) {
291
this.logService.info(`ChatService#${method}: ${message}`);
292
} else {
293
this.logService.info(`ChatService#${method}`);
294
}
295
}
296
297
private error(method: string, message: string): void {
298
this.logService.error(`ChatService#${method} ${message}`);
299
}
300
301
private deserializeChats(sessionData: string): ISerializableChatsData {
302
try {
303
const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data
304
if (!Array.isArray(arrayOfSessions)) {
305
throw new Error('Expected array');
306
}
307
308
const sessions = arrayOfSessions.reduce<ISerializableChatsData>((acc, session) => {
309
// Revive serialized markdown strings in response data
310
for (const request of session.requests) {
311
if (Array.isArray(request.response)) {
312
request.response = request.response.map((response) => {
313
if (typeof response === 'string') {
314
return new MarkdownString(response);
315
}
316
return response;
317
});
318
} else if (typeof request.response === 'string') {
319
request.response = [new MarkdownString(request.response)];
320
}
321
}
322
323
acc[session.sessionId] = normalizeSerializableChatData(session);
324
return acc;
325
}, {});
326
return sessions;
327
} catch (err) {
328
this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);
329
return {};
330
}
331
}
332
333
/**
334
* todo@connor4312 This will be cleaned up with the globalization of edits.
335
*/
336
private async reviveSessionsWithEdits(): Promise<void> {
337
const idx = await this._chatSessionStore.getIndex();
338
await Promise.all(Object.values(idx).map(async session => {
339
if (!session.hasPendingEdits) {
340
return;
341
}
342
343
const sessionResource = LocalChatSessionUri.forSession(session.sessionId);
344
const sessionRef = await this.getOrRestoreSession(sessionResource);
345
if (sessionRef?.object.editingSession) {
346
await chatEditingSessionIsReady(sessionRef.object.editingSession);
347
// the session will hold a self-reference as long as there are modified files
348
sessionRef.dispose();
349
}
350
}));
351
}
352
353
/**
354
* Returns an array of chat details for all persisted chat sessions that have at least one request.
355
* Chat sessions that have already been loaded into the chat view are excluded from the result.
356
* Imported chat sessions are also excluded from the result.
357
* TODO this is only used by the old "show chats" command which can be removed when the pre-agents view
358
* options are removed.
359
*/
360
async getLocalSessionHistory(): Promise<IChatDetail[]> {
361
const liveSessionItems = await this.getLiveSessionItems();
362
const historySessionItems = await this.getHistorySessionItems();
363
364
return [...liveSessionItems, ...historySessionItems];
365
}
366
367
/**
368
* Returns an array of chat details for all local live chat sessions.
369
*/
370
async getLiveSessionItems(): Promise<IChatDetail[]> {
371
return await Promise.all(Array.from(this._sessionModels.values())
372
.filter(session => this.shouldBeInHistory(session))
373
.map(async (session): Promise<IChatDetail> => {
374
const title = session.title || localize('newChat', "New Chat");
375
return {
376
sessionResource: session.sessionResource,
377
title,
378
lastMessageDate: session.lastMessageDate,
379
timing: session.timing,
380
isActive: true,
381
stats: await awaitStatsForSession(session),
382
lastResponseState: session.lastRequest?.response?.state ?? ResponseModelState.Pending,
383
};
384
}));
385
}
386
387
/**
388
* Returns an array of chat details for all local chat sessions in history (not currently loaded).
389
*/
390
async getHistorySessionItems(): Promise<IChatDetail[]> {
391
const index = await this._chatSessionStore.getIndex();
392
return Object.values(index)
393
.filter(entry => !entry.isExternal)
394
.filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty)
395
.map((entry): IChatDetail => {
396
const sessionResource = LocalChatSessionUri.forSession(entry.sessionId);
397
return ({
398
...entry,
399
sessionResource,
400
isActive: this._sessionModels.has(sessionResource),
401
});
402
});
403
}
404
405
async getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined> {
406
const index = await this._chatSessionStore.getIndex();
407
const metadata: IChatSessionEntryMetadata | undefined = index[sessionResource.toString()];
408
if (metadata) {
409
return {
410
...metadata,
411
sessionResource,
412
isActive: this._sessionModels.has(sessionResource),
413
};
414
}
415
416
return undefined;
417
}
418
419
private shouldBeInHistory(entry: ChatModel): boolean {
420
return !entry.isImported && !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation === ChatAgentLocation.Chat;
421
}
422
423
async removeHistoryEntry(sessionResource: URI): Promise<void> {
424
await this._chatSessionStore.deleteSession(this.toLocalSessionId(sessionResource));
425
this._onDidDisposeSession.fire({ sessionResource: [sessionResource], reason: 'cleared' });
426
}
427
428
async clearAllHistoryEntries(): Promise<void> {
429
await this._chatSessionStore.clearAllSessions();
430
}
431
432
startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference {
433
this.trace('startSession');
434
const sessionId = generateUuid();
435
const sessionResource = LocalChatSessionUri.forSession(sessionId);
436
return this._sessionModels.acquireOrCreate({
437
initialData: undefined,
438
location,
439
sessionResource,
440
sessionId,
441
canUseTools: options?.canUseTools ?? true,
442
disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive
443
});
444
}
445
446
private _startSession(props: IStartSessionProps): ChatModel {
447
const { initialData, location, sessionResource, sessionId, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props;
448
const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, sessionId, disableBackgroundKeepAlive, inputState });
449
if (location === ChatAgentLocation.Chat) {
450
model.startEditingSession(true, transferEditingSession);
451
}
452
453
this.initializeSession(model);
454
return model;
455
}
456
457
private initializeSession(model: ChatModel): void {
458
this.trace('initializeSession', `Initialize session ${model.sessionResource}`);
459
460
// Activate the default extension provided agent but do not wait
461
// for it to be ready so that the session can be used immediately
462
// without having to wait for the agent to be ready.
463
this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e));
464
}
465
466
async activateDefaultAgent(location: ChatAgentLocation): Promise<void> {
467
await this.extensionService.whenInstalledExtensionsRegistered();
468
469
const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Chat);
470
if (!defaultAgentData) {
471
throw new ErrorNoTelemetry('No default agent contributed');
472
}
473
474
// Await activation of the extension provided agent
475
// Using `activateById` as workaround for the issue
476
// https://github.com/microsoft/vscode/issues/250590
477
if (!defaultAgentData.isCore) {
478
await this.extensionService.activateById(defaultAgentData.extensionId, {
479
activationEvent: `onChatParticipant:${defaultAgentData.id}`,
480
extensionId: defaultAgentData.extensionId,
481
startup: false
482
});
483
}
484
485
const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id);
486
if (!defaultAgent) {
487
throw new ErrorNoTelemetry('No default agent registered');
488
}
489
}
490
491
getSession(sessionResource: URI): IChatModel | undefined {
492
return this._sessionModels.get(sessionResource);
493
}
494
495
getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined {
496
return this._sessionModels.acquireExisting(sessionResource);
497
}
498
499
async getOrRestoreSession(sessionResource: URI): Promise<IChatModelReference | undefined> {
500
this.trace('getOrRestoreSession', `${sessionResource}`);
501
const existingRef = this._sessionModels.acquireExisting(sessionResource);
502
if (existingRef) {
503
return existingRef;
504
}
505
506
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
507
if (!sessionId) {
508
throw new Error(`Cannot restore non-local session ${sessionResource}`);
509
}
510
511
let sessionData: ISerializedChatDataReference | undefined;
512
if (isEqual(this.transferredSessionResource, sessionResource)) {
513
this._transferredSessionResource = undefined;
514
sessionData = await this._chatSessionStore.readTransferredSession(sessionResource);
515
} else {
516
sessionData = await this._chatSessionStore.readSession(sessionId);
517
}
518
519
if (!sessionData) {
520
return undefined;
521
}
522
523
const sessionRef = this._sessionModels.acquireOrCreate({
524
initialData: sessionData,
525
location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat,
526
sessionResource,
527
sessionId,
528
canUseTools: true,
529
});
530
531
return sessionRef;
532
}
533
534
// There are some cases where this returns a real string. What happens if it doesn't?
535
// This had titles restored from the index, so just return titles from index instead, sync.
536
getSessionTitle(sessionResource: URI): string | undefined {
537
const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
538
if (!sessionId) {
539
return undefined;
540
}
541
542
return this._sessionModels.get(sessionResource)?.title ??
543
this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title;
544
}
545
546
loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined {
547
const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid();
548
const sessionResource = LocalChatSessionUri.forSession(sessionId);
549
return this._sessionModels.acquireOrCreate({
550
initialData: { value: data, serializer: new ChatSessionOperationLog() },
551
location: data.initialLocation ?? ChatAgentLocation.Chat,
552
sessionResource,
553
sessionId,
554
canUseTools: true,
555
});
556
}
557
558
async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined> {
559
// TODO: Move this into a new ChatModelService
560
561
if (chatSessionResource.scheme === Schemas.vscodeLocalChatSession) {
562
return this.getOrRestoreSession(chatSessionResource);
563
}
564
565
const existingRef = this._sessionModels.acquireExisting(chatSessionResource);
566
if (existingRef) {
567
return existingRef;
568
}
569
570
const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None);
571
const chatSessionType = chatSessionResource.scheme;
572
573
// Contributed sessions do not use UI tools
574
const modelRef = this._sessionModels.acquireOrCreate({
575
initialData: undefined,
576
location,
577
sessionResource: chatSessionResource,
578
canUseTools: false,
579
transferEditingSession: providedSession.transferredState?.editingSession,
580
inputState: providedSession.transferredState?.inputState,
581
});
582
583
modelRef.object.setContributedChatSession({
584
chatSessionResource,
585
chatSessionType,
586
isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer)
587
});
588
589
const model = modelRef.object;
590
const disposables = new DisposableStore();
591
disposables.add(modelRef.object.onDidDispose(() => {
592
disposables.dispose();
593
providedSession.dispose();
594
}));
595
596
let lastRequest: ChatRequestModel | undefined;
597
for (const message of providedSession.history) {
598
if (message.type === 'request') {
599
if (lastRequest) {
600
lastRequest.response?.complete();
601
}
602
603
const requestText = message.prompt;
604
605
const parsedRequest: IParsedChatRequest = {
606
text: requestText,
607
parts: [new ChatRequestTextPart(
608
new OffsetRange(0, requestText.length),
609
{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 },
610
requestText
611
)]
612
};
613
const agent =
614
message.participant
615
? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode?
616
: this.chatAgentService.getAgent(chatSessionType);
617
lastRequest = model.addRequest(parsedRequest,
618
message.variableData ?? { variables: [] },
619
0, // attempt
620
undefined,
621
agent,
622
undefined, // slashCommand
623
undefined, // confirmation
624
undefined, // locationData
625
undefined, // attachments
626
false, // Do not treat as requests completed, else edit pills won't show.
627
undefined,
628
undefined,
629
message.id
630
);
631
} else {
632
// response
633
if (lastRequest) {
634
for (const part of message.parts) {
635
model.acceptResponseProgress(lastRequest, part);
636
}
637
}
638
}
639
}
640
641
if (providedSession.isCompleteObs?.get()) {
642
lastRequest?.response?.complete();
643
}
644
645
if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) {
646
const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
647
this._pendingRequests.set(model.sessionResource, initialCancellationRequest);
648
const cancellationListener = disposables.add(new MutableDisposable());
649
650
const createCancellationListener = (token: CancellationToken) => {
651
return token.onCancellationRequested(() => {
652
providedSession.interruptActiveResponseCallback?.().then(userConfirmedInterruption => {
653
if (!userConfirmedInterruption) {
654
// User cancelled the interruption
655
const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
656
this._pendingRequests.set(model.sessionResource, newCancellationRequest);
657
cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token);
658
}
659
});
660
});
661
};
662
663
cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token);
664
665
let lastProgressLength = 0;
666
disposables.add(autorun(reader => {
667
const progressArray = providedSession.progressObs?.read(reader) ?? [];
668
const isComplete = providedSession.isCompleteObs?.read(reader) ?? false;
669
670
// Process only new progress items
671
if (progressArray.length > lastProgressLength) {
672
const newProgress = progressArray.slice(lastProgressLength);
673
for (const progress of newProgress) {
674
model?.acceptResponseProgress(lastRequest, progress);
675
}
676
lastProgressLength = progressArray.length;
677
}
678
679
// Handle completion
680
if (isComplete) {
681
lastRequest.response?.complete();
682
cancellationListener.clear();
683
}
684
}));
685
} else {
686
if (lastRequest && model.editingSession) {
687
// wait for timeline to load so that a 'changes' part is added when the response completes
688
await chatEditingSessionIsReady(model.editingSession);
689
lastRequest.response?.complete();
690
}
691
}
692
693
return modelRef;
694
}
695
696
getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined {
697
const model = this._sessionModels.get(sessionResource);
698
if (!model) {
699
return;
700
}
701
const { contributedChatSession } = model;
702
return contributedChatSession;
703
}
704
705
async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise<void> {
706
const model = this._sessionModels.get(request.session.sessionResource);
707
if (!model && model !== request.session) {
708
throw new Error(`Unknown session: ${request.session.sessionResource}`);
709
}
710
711
const cts = this._pendingRequests.get(request.session.sessionResource);
712
if (cts) {
713
this.trace('resendRequest', `Session ${request.session.sessionResource} already has a pending request, cancelling...`);
714
cts.cancel();
715
}
716
717
const location = options?.location ?? model.initialLocation;
718
const attempt = options?.attempt ?? 0;
719
const enableCommandDetection = !options?.noCommandDetection;
720
const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;
721
722
model.removeRequest(request.id, ChatRequestRemovalReason.Resend);
723
724
const resendOptions: IChatSendRequestOptions = {
725
...options,
726
locationData: request.locationData,
727
attachedContext: request.attachedContext,
728
};
729
await this._sendRequestAsync(model, model.sessionResource, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise;
730
}
731
732
private queuePendingRequest(model: ChatModel, sessionResource: URI, request: string, options: IChatSendRequestOptions): ChatSendResultQueued {
733
const location = options.location ?? model.initialLocation;
734
const parsedRequest = this.parseChatRequest(sessionResource, request, location, options);
735
const requestModel = new ChatRequestModel({
736
session: model,
737
message: parsedRequest,
738
variableData: { variables: options.attachedContext ?? [] },
739
timestamp: Date.now(),
740
modeInfo: options.modeInfo,
741
locationData: options.locationData,
742
attachedContext: options.attachedContext,
743
modelId: options.userSelectedModelId,
744
userSelectedTools: options.userSelectedTools?.get(),
745
});
746
747
const deferred = new DeferredPromise<ChatSendResult>();
748
this._queuedRequestDeferreds.set(requestModel.id, deferred);
749
750
model.addPendingRequest(requestModel, options.queue ?? ChatRequestQueueKind.Queued, { ...options, queue: undefined });
751
752
if (options.queue === ChatRequestQueueKind.Steering) {
753
this.setYieldRequested(sessionResource);
754
}
755
756
this.trace('sendRequest', `Queued message for session ${sessionResource}`);
757
return { kind: 'queued', deferred: deferred.p };
758
}
759
760
async sendRequest(sessionResource: URI, request: string, options?: IChatSendRequestOptions): Promise<ChatSendResult> {
761
this.trace('sendRequest', `sessionResource: ${sessionResource.toString()}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
762
763
764
if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) {
765
this.trace('sendRequest', 'Rejected empty message');
766
return { kind: 'rejected', reason: 'Empty message' };
767
}
768
769
const model = this._sessionModels.get(sessionResource);
770
if (!model) {
771
throw new Error(`Unknown session: ${sessionResource}`);
772
}
773
774
const hasPendingRequest = this._pendingRequests.has(sessionResource);
775
const hasPendingQueue = model.getPendingRequests().length > 0;
776
777
if (options?.queue) {
778
return this.queuePendingRequest(model, sessionResource, request, options);
779
} else if (hasPendingRequest) {
780
this.trace('sendRequest', `Session ${sessionResource} already has a pending request`);
781
return { kind: 'rejected', reason: 'Request already in progress' };
782
}
783
784
if (options?.queue && hasPendingQueue) {
785
const queued = this.queuePendingRequest(model, sessionResource, request, options);
786
this.processNextPendingRequest(model);
787
return queued;
788
}
789
790
const requests = model.getRequests();
791
for (let i = requests.length - 1; i >= 0; i -= 1) {
792
const request = requests[i];
793
if (request.shouldBeRemovedOnSend) {
794
if (request.shouldBeRemovedOnSend.afterUndoStop) {
795
request.response?.finalizeUndoState();
796
} else {
797
await this.removeRequest(sessionResource, request.id);
798
}
799
}
800
}
801
802
const location = options?.location ?? model.initialLocation;
803
const attempt = options?.attempt ?? 0;
804
const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;
805
806
const parsedRequest = this.parseChatRequest(sessionResource, request, location, options);
807
const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined;
808
const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;
809
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
810
811
// This method is only returning whether the request was accepted - don't block on the actual request
812
return {
813
kind: 'sent',
814
data: {
815
...this._sendRequestAsync(model, sessionResource, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options),
816
agent,
817
slashCommand: agentSlashCommandPart?.command,
818
},
819
};
820
}
821
822
private parseChatRequest(sessionResource: URI, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest {
823
let parserContext = options?.parserContext;
824
if (options?.agentId) {
825
const agent = this.chatAgentService.getAgent(options.agentId);
826
if (!agent) {
827
throw new Error(`Unknown agent: ${options.agentId}`);
828
}
829
parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind };
830
const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : '';
831
request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`;
832
}
833
834
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, request, location, parserContext);
835
return parsedRequest;
836
}
837
838
private refreshFollowupsCancellationToken(sessionResource: URI): CancellationToken {
839
this._sessionFollowupCancelTokens.get(sessionResource)?.cancel();
840
const newTokenSource = new CancellationTokenSource();
841
this._sessionFollowupCancelTokens.set(sessionResource, newTokenSource);
842
843
return newTokenSource.token;
844
}
845
846
private _sendRequestAsync(model: ChatModel, sessionResource: URI, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {
847
const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionResource);
848
let request: ChatRequestModel;
849
const agentPart = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
850
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
851
const commandPart = parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);
852
const requests = [...model.getRequests()];
853
const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, {
854
agent: agentPart?.agent ?? defaultAgent,
855
agentSlashCommandPart,
856
commandPart,
857
sessionId: model.sessionId,
858
location: model.initialLocation,
859
options,
860
enableCommandDetection
861
});
862
863
let gotProgress = false;
864
const requestType = commandPart ? 'slashCommand' : 'string';
865
866
const responseCreated = new DeferredPromise<IChatResponseModel>();
867
let responseCreatedComplete = false;
868
function completeResponseCreated(): void {
869
if (!responseCreatedComplete && request?.response) {
870
responseCreated.complete(request.response);
871
responseCreatedComplete = true;
872
}
873
}
874
875
const store = new DisposableStore();
876
const source = store.add(new CancellationTokenSource());
877
const token = source.token;
878
const sendRequestInternal = async () => {
879
const progressCallback = (progress: IChatProgress[]) => {
880
if (token.isCancellationRequested) {
881
return;
882
}
883
884
gotProgress = true;
885
886
for (let i = 0; i < progress.length; i++) {
887
const isLast = i === progress.length - 1;
888
const progressItem = progress[i];
889
890
if (progressItem.kind === 'markdownContent') {
891
this.trace('sendRequest', `Provider returned progress for session ${model.sessionResource}, ${progressItem.content.value.length} chars`);
892
} else {
893
this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`);
894
}
895
896
model.acceptResponseProgress(request, progressItem, !isLast);
897
}
898
completeResponseCreated();
899
};
900
901
let detectedAgent: IChatAgentData | undefined;
902
let detectedCommand: IChatAgentCommand | undefined;
903
904
// Collect hooks from hook .json files
905
let collectedHooks: IChatRequestHooks | undefined;
906
try {
907
collectedHooks = await this.promptsService.getHooks(token);
908
} catch (error) {
909
this.logService.warn('[ChatService] Failed to collect hooks:', error);
910
}
911
912
const stopWatch = new StopWatch(false);
913
store.add(token.onCancellationRequested(() => {
914
this.trace('sendRequest', `Request for session ${model.sessionResource} was cancelled`);
915
if (!request) {
916
return;
917
}
918
919
requestTelemetry.complete({
920
timeToFirstProgress: undefined,
921
result: 'cancelled',
922
// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling
923
totalTime: stopWatch.elapsed(),
924
requestType,
925
detectedAgent,
926
request,
927
});
928
929
model.cancelRequest(request);
930
}));
931
932
try {
933
let rawResult: IChatAgentResult | null | undefined;
934
let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;
935
if (agentPart || (defaultAgent && !commandPart)) {
936
const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => {
937
const initVariableData: IChatRequestVariableData = { variables: [] };
938
request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId, options?.userSelectedTools?.get());
939
940
let variableData: IChatRequestVariableData;
941
let message: string;
942
if (chatRequest) {
943
variableData = chatRequest.variableData;
944
message = getPromptText(request.message).message;
945
} else {
946
variableData = { variables: this.prepareContext(request.attachedContext) };
947
model.updateRequest(request, variableData);
948
949
// Merge resolved variables (e.g. images from directories) for the
950
// agent request only - they are not stored on the request model.
951
if (options?.resolvedVariables?.length) {
952
variableData = { variables: [...variableData.variables, ...options.resolvedVariables] };
953
}
954
955
const promptTextResult = getPromptText(request.message);
956
variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack
957
message = promptTextResult.message;
958
}
959
960
const agentRequest: IChatAgentRequest = {
961
sessionResource: model.sessionResource,
962
requestId: request.id,
963
agentId: agent.id,
964
message,
965
command: command?.name,
966
variables: variableData,
967
enableCommandDetection,
968
isParticipantDetected,
969
attempt,
970
location,
971
locationData: request.locationData,
972
acceptedConfirmationData: options?.acceptedConfirmationData,
973
rejectedConfirmationData: options?.rejectedConfirmationData,
974
userSelectedModelId: options?.userSelectedModelId,
975
userSelectedTools: options?.userSelectedTools?.get(),
976
modeInstructions: options?.modeInfo?.modeInstructions,
977
editedFileEvents: request.editedFileEvents,
978
hooks: collectedHooks,
979
hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0),
980
};
981
982
let isInitialTools = true;
983
984
store.add(autorun(reader => {
985
const tools = options?.userSelectedTools?.read(reader);
986
if (isInitialTools) {
987
isInitialTools = false;
988
return;
989
}
990
991
if (tools) {
992
this.chatAgentService.setRequestTools(agent.id, request.id, tools);
993
// in case the request has not been sent out yet:
994
agentRequest.userSelectedTools = tools;
995
}
996
}));
997
998
return agentRequest;
999
};
1000
1001
if (
1002
this.configurationService.getValue('chat.detectParticipant.enabled') !== false &&
1003
this.chatAgentService.hasChatParticipantDetectionProviders() &&
1004
!agentPart &&
1005
!commandPart &&
1006
!agentSlashCommandPart &&
1007
enableCommandDetection &&
1008
(location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) &&
1009
options?.modeInfo?.kind !== ChatModeKind.Agent &&
1010
options?.modeInfo?.kind !== ChatModeKind.Edit &&
1011
!options?.agentIdSilent
1012
) {
1013
// We have no agent or command to scope history with, pass the full history to the participant detection provider
1014
const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, location, defaultAgent.id);
1015
1016
// Prepare the request object that we will send to the participant detection provider
1017
const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false);
1018
1019
const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token);
1020
if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) {
1021
// Update the response in the ChatModel to reflect the detected agent and command
1022
request.response?.setAgent(result.agent, result.command);
1023
detectedAgent = result.agent;
1024
detectedCommand = result.command;
1025
}
1026
}
1027
1028
const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!;
1029
const command = detectedCommand ?? agentSlashCommandPart?.command;
1030
1031
await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`);
1032
1033
// Recompute history in case the agent or command changed
1034
const history = this.getHistoryEntriesFromModel(requests, location, agent.id);
1035
const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent);
1036
this.generateInitialChatTitleIfNeeded(model, requestProps, defaultAgent, token);
1037
const pendingRequest = this._pendingRequests.get(sessionResource);
1038
if (pendingRequest) {
1039
store.add(autorun(reader => {
1040
if (pendingRequest.yieldRequested.read(reader)) {
1041
this.chatAgentService.setYieldRequested(agent.id, request.id);
1042
}
1043
}));
1044
pendingRequest.requestId ??= requestProps.requestId;
1045
}
1046
completeResponseCreated();
1047
1048
// MCP autostart: only run for native VS Code sessions (sidebar, new editors) but not for extension contributed sessions that have inputType set.
1049
if (model.canUseTools) {
1050
const autostartResult = new ChatMcpServersStarting(this.mcpService.autostart(token));
1051
if (!autostartResult.isEmpty) {
1052
progressCallback([autostartResult]);
1053
await autostartResult.wait();
1054
}
1055
}
1056
1057
const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);
1058
rawResult = agentResult;
1059
agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);
1060
} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {
1061
if (commandPart.slashCommand.silent !== true) {
1062
request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo);
1063
completeResponseCreated();
1064
}
1065
// contributed slash commands
1066
// TODO: spell this out in the UI
1067
const history: IChatMessage[] = [];
1068
for (const modelRequest of model.getRequests()) {
1069
if (!modelRequest.response) {
1070
continue;
1071
}
1072
history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] });
1073
history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] });
1074
}
1075
const message = parsedRequest.text;
1076
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {
1077
progressCallback([p]);
1078
}), history, location, model.sessionResource, token);
1079
agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);
1080
rawResult = {};
1081
1082
} else {
1083
throw new Error(`Cannot handle request`);
1084
}
1085
1086
if (token.isCancellationRequested && !rawResult) {
1087
return;
1088
} else {
1089
if (!rawResult) {
1090
this.trace('sendRequest', `Provider returned no response for session ${model.sessionResource}`);
1091
rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };
1092
}
1093
1094
const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' :
1095
rawResult.errorDetails && gotProgress ? 'errorWithOutput' :
1096
rawResult.errorDetails ? 'error' :
1097
'success';
1098
1099
requestTelemetry.complete({
1100
timeToFirstProgress: rawResult.timings?.firstProgress,
1101
totalTime: rawResult.timings?.totalElapsed,
1102
result,
1103
requestType,
1104
detectedAgent,
1105
request,
1106
});
1107
1108
model.setResponse(request, rawResult);
1109
completeResponseCreated();
1110
this.trace('sendRequest', `Provider returned response for session ${model.sessionResource}`);
1111
1112
shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested;
1113
request.response?.complete();
1114
if (agentOrCommandFollowups) {
1115
agentOrCommandFollowups.then(followups => {
1116
model.setFollowups(request, followups);
1117
const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command;
1118
this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0);
1119
});
1120
}
1121
}
1122
} catch (err) {
1123
this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`);
1124
requestTelemetry.complete({
1125
timeToFirstProgress: undefined,
1126
totalTime: undefined,
1127
result: 'error',
1128
requestType,
1129
detectedAgent,
1130
request,
1131
});
1132
if (request) {
1133
const rawResult: IChatAgentResult = { errorDetails: { message: err.message } };
1134
model.setResponse(request, rawResult);
1135
completeResponseCreated();
1136
request.response?.complete();
1137
}
1138
} finally {
1139
store.dispose();
1140
}
1141
};
1142
let shouldProcessPending = false;
1143
const rawResponsePromise = sendRequestInternal();
1144
// Note- requestId is not known at this point, assigned later
1145
this._pendingRequests.set(model.sessionResource, this.instantiationService.createInstance(CancellableRequest, source, undefined));
1146
rawResponsePromise.finally(() => {
1147
this._pendingRequests.deleteAndDispose(model.sessionResource);
1148
// Process the next pending request from the queue if any
1149
if (shouldProcessPending) {
1150
this.processNextPendingRequest(model);
1151
}
1152
});
1153
this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource });
1154
return {
1155
responseCreatedPromise: responseCreated.p,
1156
responseCompletePromise: rawResponsePromise,
1157
};
1158
}
1159
1160
processPendingRequests(sessionResource: URI): void {
1161
const model = this._sessionModels.get(sessionResource);
1162
if (model && !this._pendingRequests.has(sessionResource)) {
1163
this.processNextPendingRequest(model);
1164
}
1165
}
1166
1167
/**
1168
* Process the next pending request from the model's queue, if any.
1169
* Called after a request completes to continue processing queued requests.
1170
*/
1171
private processNextPendingRequest(model: ChatModel): void {
1172
const pendingRequest = model.dequeuePendingRequest();
1173
if (!pendingRequest) {
1174
return;
1175
}
1176
1177
this.trace('processNextPendingRequest', `Processing queued request for session ${model.sessionResource}`);
1178
1179
const deferred = this._queuedRequestDeferreds.get(pendingRequest.request.id);
1180
this._queuedRequestDeferreds.delete(pendingRequest.request.id);
1181
1182
const sendOptions: IChatSendRequestOptions = {
1183
...pendingRequest.sendOptions,
1184
// Ensure attachedContext is preserved after deserialization, where sendOptions
1185
// loses attachedContext but the request model retains it in variableData.
1186
attachedContext: pendingRequest.request.variableData.variables.slice(),
1187
};
1188
const location = sendOptions.location ?? sendOptions.locationData?.type ?? model.initialLocation;
1189
const defaultAgent = this.chatAgentService.getDefaultAgent(location, sendOptions.modeInfo?.kind);
1190
if (!defaultAgent) {
1191
this.logService.warn('processNextPendingRequest', `No default agent for location ${location}`);
1192
deferred?.complete({ kind: 'rejected', reason: 'No default agent available' });
1193
return;
1194
}
1195
1196
const parsedRequest = pendingRequest.request.message;
1197
const silentAgent = sendOptions.agentIdSilent ? this.chatAgentService.getAgent(sendOptions.agentIdSilent) : undefined;
1198
const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;
1199
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
1200
1201
// Send the queued request - this will add it to _pendingRequests and handle it normally
1202
const responseState = this._sendRequestAsync(model, model.sessionResource, parsedRequest, pendingRequest.request.attempt, !sendOptions.noCommandDetection, silentAgent ?? defaultAgent, location, sendOptions);
1203
1204
// Resolve the deferred with the sent result
1205
deferred?.complete({
1206
kind: 'sent',
1207
data: {
1208
...responseState,
1209
agent,
1210
slashCommand: agentSlashCommandPart?.command,
1211
},
1212
});
1213
}
1214
1215
private generateInitialChatTitleIfNeeded(model: ChatModel, request: IChatAgentRequest, defaultAgent: IChatAgentData, token: CancellationToken): void {
1216
// Generate a title only for the first request, and only via the default agent.
1217
// Use a single-entry history based on the current request (no full chat history).
1218
if (model.getRequests().length !== 1 || model.customTitle) {
1219
return;
1220
}
1221
1222
const singleEntryHistory: IChatAgentHistoryEntry[] = [{
1223
request,
1224
response: [],
1225
result: {}
1226
}];
1227
const generate = async () => {
1228
const title = await this.chatAgentService.getChatTitle(defaultAgent.id, singleEntryHistory, token);
1229
if (title && !model.customTitle) {
1230
model.setCustomTitle(title);
1231
}
1232
};
1233
void generate();
1234
}
1235
1236
private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] {
1237
attachedContextVariables ??= [];
1238
1239
// "reverse", high index first so that replacement is simple
1240
attachedContextVariables.sort((a, b) => {
1241
// If either range is undefined, sort it to the back
1242
if (!a.range && !b.range) {
1243
return 0; // Keep relative order if both ranges are undefined
1244
}
1245
if (!a.range) {
1246
return 1; // a goes after b
1247
}
1248
if (!b.range) {
1249
return -1; // a goes before b
1250
}
1251
return b.range.start - a.range.start;
1252
});
1253
1254
return attachedContextVariables;
1255
}
1256
1257
private getHistoryEntriesFromModel(requests: IChatRequestModel[], location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] {
1258
const history: IChatAgentHistoryEntry[] = [];
1259
const agent = this.chatAgentService.getAgent(forAgentId);
1260
for (const request of requests) {
1261
if (!request.response) {
1262
continue;
1263
}
1264
1265
if (forAgentId !== request.response.agent?.id && !agent?.isDefault && !agent?.canAccessPreviousChatHistory) {
1266
// An agent only gets to see requests that were sent to this agent.
1267
// The default agent (the undefined case), or agents with 'canAccessPreviousChatHistory', get to see all of them.
1268
continue;
1269
}
1270
1271
// Do not save to history inline completions
1272
if (location === ChatAgentLocation.EditorInline) {
1273
continue;
1274
}
1275
1276
const promptTextResult = getPromptText(request.message);
1277
const historyRequest: IChatAgentRequest = {
1278
sessionResource: request.session.sessionResource,
1279
requestId: request.id,
1280
agentId: request.response.agent?.id ?? '',
1281
message: promptTextResult.message,
1282
command: request.response.slashCommand?.name,
1283
variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack
1284
location: ChatAgentLocation.Chat,
1285
editedFileEvents: request.editedFileEvents,
1286
};
1287
history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} });
1288
}
1289
1290
return history;
1291
}
1292
1293
async removeRequest(sessionResource: URI, requestId: string): Promise<void> {
1294
const model = this._sessionModels.get(sessionResource);
1295
if (!model) {
1296
throw new Error(`Unknown session: ${sessionResource}`);
1297
}
1298
1299
const pendingRequest = this._pendingRequests.get(sessionResource);
1300
if (pendingRequest?.requestId === requestId) {
1301
pendingRequest.cancel();
1302
this._pendingRequests.deleteAndDispose(sessionResource);
1303
}
1304
1305
model.removeRequest(requestId);
1306
}
1307
1308
async adoptRequest(sessionResource: URI, request: IChatRequestModel) {
1309
if (!(request instanceof ChatRequestModel)) {
1310
throw new TypeError('Can only adopt requests of type ChatRequestModel');
1311
}
1312
const target = this._sessionModels.get(sessionResource);
1313
if (!target) {
1314
throw new Error(`Unknown session: ${sessionResource}`);
1315
}
1316
1317
const oldOwner = request.session;
1318
target.adoptRequest(request);
1319
1320
if (request.response && !request.response.isComplete) {
1321
const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionResource);
1322
if (cts) {
1323
cts.requestId = request.id;
1324
this._pendingRequests.set(target.sessionResource, cts);
1325
}
1326
}
1327
}
1328
1329
async addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise<void> {
1330
this.trace('addCompleteRequest', `message: ${message}`);
1331
1332
const model = this._sessionModels.get(sessionResource);
1333
if (!model) {
1334
throw new Error(`Unknown session: ${sessionResource}`);
1335
}
1336
1337
const parsedRequest = typeof message === 'string' ?
1338
this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, message) :
1339
message;
1340
const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true);
1341
if (typeof response.message === 'string') {
1342
// TODO is this possible?
1343
model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' });
1344
} else {
1345
for (const part of response.message) {
1346
model.acceptResponseProgress(request, part, true);
1347
}
1348
}
1349
model.setResponse(request, response.result || {});
1350
if (response.followups !== undefined) {
1351
model.setFollowups(request, response.followups);
1352
}
1353
request.response?.complete();
1354
}
1355
1356
cancelCurrentRequestForSession(sessionResource: URI): void {
1357
this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`);
1358
this._pendingRequests.get(sessionResource)?.cancel();
1359
this._pendingRequests.deleteAndDispose(sessionResource);
1360
}
1361
1362
setYieldRequested(sessionResource: URI): void {
1363
const pendingRequest = this._pendingRequests.get(sessionResource);
1364
if (pendingRequest) {
1365
pendingRequest.setYieldRequested();
1366
}
1367
}
1368
1369
removePendingRequest(sessionResource: URI, requestId: string): void {
1370
const model = this._sessionModels.get(sessionResource) as ChatModel | undefined;
1371
if (model) {
1372
model.removePendingRequest(requestId);
1373
}
1374
1375
// Reject the deferred promise for the removed request
1376
const deferred = this._queuedRequestDeferreds.get(requestId);
1377
if (deferred) {
1378
deferred.complete({ kind: 'rejected', reason: 'Request was removed from queue' });
1379
this._queuedRequestDeferreds.delete(requestId);
1380
}
1381
}
1382
1383
setPendingRequests(sessionResource: URI, requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void {
1384
const model = this._sessionModels.get(sessionResource) as ChatModel | undefined;
1385
if (model) {
1386
model.setPendingRequests(requests);
1387
}
1388
}
1389
1390
public hasSessions(): boolean {
1391
return this._chatSessionStore.hasSessions();
1392
}
1393
1394
async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise<void> {
1395
if (!LocalChatSessionUri.isLocalSession(transferredSessionResource)) {
1396
throw new Error(`Can only transfer local chat sessions. Invalid session: ${transferredSessionResource}`);
1397
}
1398
1399
const model = this._sessionModels.get(transferredSessionResource) as ChatModel | undefined;
1400
if (!model) {
1401
throw new Error(`Failed to transfer session. Unknown session: ${transferredSessionResource}`);
1402
}
1403
1404
if (model.initialLocation !== ChatAgentLocation.Chat) {
1405
throw new Error(`Can only transfer chat sessions located in the Chat view. Session ${transferredSessionResource} has location=${model.initialLocation}`);
1406
}
1407
1408
await this._chatSessionStore.storeTransferSession({
1409
sessionResource: model.sessionResource,
1410
timestampInMilliseconds: Date.now(),
1411
toWorkspace: toWorkspace,
1412
}, model);
1413
this.chatTransferService.addWorkspaceToTransferred(toWorkspace);
1414
this.trace('transferChatSession', `Transferred session ${model.sessionResource} to workspace ${toWorkspace.toString()}`);
1415
}
1416
1417
getChatStorageFolder(): URI {
1418
return this._chatSessionStore.getChatStorageFolder();
1419
}
1420
1421
logChatIndex(): void {
1422
this._chatSessionStore.logIndex();
1423
}
1424
1425
setTitle(sessionResource: URI, title: string): void {
1426
this._sessionModels.get(sessionResource)?.setCustomTitle(title);
1427
}
1428
1429
appendProgress(request: IChatRequestModel, progress: IChatProgress): void {
1430
const model = this._sessionModels.get(request.session.sessionResource);
1431
if (!(request instanceof ChatRequestModel)) {
1432
throw new BugIndicatingError('Can only append progress to requests of type ChatRequestModel');
1433
}
1434
1435
model?.acceptResponseProgress(request, progress);
1436
}
1437
1438
private toLocalSessionId(sessionResource: URI) {
1439
const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);
1440
if (!localSessionId) {
1441
throw new Error(`Invalid local chat session resource: ${sessionResource}`);
1442
}
1443
return localSessionId;
1444
}
1445
}
1446
1447