Path: blob/main/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts
5263 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { DeferredPromise } from '../../../../../base/common/async.js';6import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';7import { toErrorMessage } from '../../../../../base/common/errorMessage.js';8import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js';9import { Emitter, Event } from '../../../../../base/common/event.js';10import { MarkdownString } from '../../../../../base/common/htmlContent.js';11import { Iterable } from '../../../../../base/common/iterator.js';12import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';13import { revive } from '../../../../../base/common/marshalling.js';14import { Schemas } from '../../../../../base/common/network.js';15import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';16import { isEqual } from '../../../../../base/common/resources.js';17import { StopWatch } from '../../../../../base/common/stopwatch.js';18import { isDefined } from '../../../../../base/common/types.js';19import { URI } from '../../../../../base/common/uri.js';20import { generateUuid } from '../../../../../base/common/uuid.js';21import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';22import { localize } from '../../../../../nls.js';23import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';24import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';25import { ILogService } from '../../../../../platform/log/common/log.js';26import { Progress } from '../../../../../platform/progress/common/progress.js';27import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';28import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';29import { IExtensionService } from '../../../../services/extensions/common/extensions.js';30import { InlineChatConfigKeys } from '../../../inlineChat/common/inlineChat.js';31import { IMcpService } from '../../../mcp/common/mcpTypes.js';32import { awaitStatsForSession } from '../chat.js';33import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../participants/chatAgents.js';34import { chatEditingSessionIsReady } from '../editing/chatEditingService.js';35import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from '../model/chatModel.js';36import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js';37import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js';38import { ChatRequestParser } from '../requestParser/chatRequestParser.js';39import { ChatMcpServersStarting, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js';40import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';41import { IChatSessionsService } from '../chatSessionsService.js';42import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js';43import { IChatSlashCommandService } from '../participants/chatSlashCommands.js';44import { IChatTransferService } from '../model/chatTransferService.js';45import { LocalChatSessionUri } from '../model/chatUri.js';46import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';47import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js';48import { ChatMessageRole, IChatMessage } from '../languageModels.js';49import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js';50import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js';51import { IPromptsService } from '../promptSyntax/service/promptsService.js';52import { IChatRequestHooks } from '../promptSyntax/hookSchema.js';5354const serializedChatKey = 'interactive.sessions';5556class CancellableRequest implements IDisposable {57private readonly _yieldRequested: ISettableObservable<boolean> = observableValue(this, false);5859get yieldRequested(): IObservable<boolean> {60return this._yieldRequested;61}6263constructor(64public readonly cancellationTokenSource: CancellationTokenSource,65public requestId: string | undefined,66@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService67) { }6869dispose() {70this.cancellationTokenSource.dispose();71}7273cancel() {74if (this.requestId) {75this.toolsService.cancelToolCallsForRequest(this.requestId);76}7778this.cancellationTokenSource.cancel();79}8081setYieldRequested(): void {82this._yieldRequested.set(true, undefined);83}84}8586export class ChatService extends Disposable implements IChatService {87declare _serviceBrand: undefined;8889private readonly _sessionModels: ChatModelStore;90private readonly _pendingRequests = this._register(new DisposableResourceMap<CancellableRequest>());91private readonly _queuedRequestDeferreds = new Map<string, DeferredPromise<ChatSendResult>>();92private _saveModelsEnabled = true;9394private _transferredSessionResource: URI | undefined;95public get transferredSessionResource(): URI | undefined {96return this._transferredSessionResource;97}9899private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>());100public readonly onDidSubmitRequest = this._onDidSubmitRequest.event;101102public get onDidCreateModel() { return this._sessionModels.onDidCreateModel; }103104private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());105public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;106107private readonly _onDidReceiveQuestionCarouselAnswer = this._register(new Emitter<{ requestId: string; resolveId: string; answers: Record<string, unknown> | undefined }>());108public readonly onDidReceiveQuestionCarouselAnswer = this._onDidReceiveQuestionCarouselAnswer.event;109110private readonly _onDidDisposeSession = this._register(new Emitter<{ readonly sessionResource: URI[]; reason: 'cleared' }>());111public readonly onDidDisposeSession = this._onDidDisposeSession.event;112113private readonly _sessionFollowupCancelTokens = this._register(new DisposableResourceMap<CancellationTokenSource>());114private readonly _chatServiceTelemetry: ChatServiceTelemetry;115private readonly _chatSessionStore: ChatSessionStore;116117readonly requestInProgressObs: IObservable<boolean>;118119readonly chatModels: IObservable<Iterable<IChatModel>>;120121/**122* For test use only123*/124setSaveModelsEnabled(enabled: boolean): void {125this._saveModelsEnabled = enabled;126}127128/**129* For test use only130*/131waitForModelDisposals(): Promise<void> {132return this._sessionModels.waitForModelDisposals();133}134135public get edits2Enabled(): boolean {136return this.configurationService.getValue(ChatConfiguration.Edits2Enabled);137}138139private get isEmptyWindow(): boolean {140const workspace = this.workspaceContextService.getWorkspace();141return !workspace.configuration && workspace.folders.length === 0;142}143144constructor(145@IStorageService private readonly storageService: IStorageService,146@ILogService private readonly logService: ILogService,147@IExtensionService private readonly extensionService: IExtensionService,148@IInstantiationService private readonly instantiationService: IInstantiationService,149@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,150@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,151@IChatAgentService private readonly chatAgentService: IChatAgentService,152@IConfigurationService private readonly configurationService: IConfigurationService,153@IChatTransferService private readonly chatTransferService: IChatTransferService,154@IChatSessionsService private readonly chatSessionService: IChatSessionsService,155@IMcpService private readonly mcpService: IMcpService,156@IPromptsService private readonly promptsService: IPromptsService,157) {158super();159160this._sessionModels = this._register(instantiationService.createInstance(ChatModelStore, {161createModel: (props: IStartSessionProps) => this._startSession(props),162willDisposeModel: async (model: ChatModel) => {163const localSessionId = LocalChatSessionUri.parseLocalSessionId(model.sessionResource);164if (localSessionId && this.shouldStoreSession(model)) {165// Always preserve sessions that have custom titles, even if empty166if (model.getRequests().length === 0 && !model.customTitle) {167await this._chatSessionStore.deleteSession(localSessionId);168} else if (this._saveModelsEnabled) {169await this._chatSessionStore.storeSessions([model]);170}171} else if (!localSessionId && model.getRequests().length > 0) {172await this._chatSessionStore.storeSessionsMetadataOnly([model]);173}174}175}));176this._register(this._sessionModels.onDidDisposeModel(model => {177this._onDidDisposeSession.fire({ sessionResource: [model.sessionResource], reason: 'cleared' });178}));179180this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry);181this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));182this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData());183184const transferredData = this._chatSessionStore.getTransferredSessionData();185if (transferredData) {186this.trace('constructor', `Transferred session ${transferredData}`);187this._transferredSessionResource = transferredData;188}189190this.reviveSessionsWithEdits();191192this._register(storageService.onWillSaveState(() => this.saveState()));193194this.chatModels = derived(this, reader => [...this._sessionModels.observable.read(reader).values()]);195196this.requestInProgressObs = derived(reader => {197const models = this._sessionModels.observable.read(reader).values();198return Iterable.some(models, model => model.requestInProgress.read(reader));199});200}201202public get editingSessions() {203return [...this._sessionModels.values()].map(v => v.editingSession).filter(isDefined);204}205206isEnabled(location: ChatAgentLocation): boolean {207return this.chatAgentService.getContributedDefaultAgent(location) !== undefined;208}209210private migrateData(): ISerializableChatsData | undefined {211const sessionData = this.storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');212if (sessionData) {213const persistedSessions = this.deserializeChats(sessionData);214const countsForLog = Object.keys(persistedSessions).length;215if (countsForLog > 0) {216this.info('migrateData', `Restored ${countsForLog} persisted sessions`);217}218219return persistedSessions;220}221222return;223}224225private saveState(): void {226if (!this._saveModelsEnabled) {227return;228}229230const liveLocalChats = Array.from(this._sessionModels.values())231.filter(session => this.shouldStoreSession(session));232233this._chatSessionStore.storeSessions(liveLocalChats);234235const liveNonLocalChats = Array.from(this._sessionModels.values())236.filter(session => !LocalChatSessionUri.parseLocalSessionId(session.sessionResource));237this._chatSessionStore.storeSessionsMetadataOnly(liveNonLocalChats);238}239240/**241* Only persist local sessions from chat that are not imported.242*/243private shouldStoreSession(session: ChatModel): boolean {244if (!LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) {245return false;246}247return session.initialLocation === ChatAgentLocation.Chat && !session.isImported;248}249250notifyUserAction(action: IChatUserActionEvent): void {251this._chatServiceTelemetry.notifyUserAction(action);252this._onDidPerformUserAction.fire(action);253if (action.action.kind === 'chatEditingSessionAction') {254const model = this._sessionModels.get(action.sessionResource);255if (model) {256model.notifyEditingAction(action.action);257}258}259}260261notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record<string, unknown> | undefined): void {262this._onDidReceiveQuestionCarouselAnswer.fire({ requestId, resolveId, answers });263}264265async setChatSessionTitle(sessionResource: URI, title: string): Promise<void> {266const model = this._sessionModels.get(sessionResource);267if (model) {268model.setCustomTitle(title);269}270271// Update the title in the file storage272const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);273if (localSessionId) {274await this._chatSessionStore.setSessionTitle(localSessionId, title);275// Trigger immediate save to ensure consistency276this.saveState();277}278}279280private trace(method: string, message?: string): void {281if (message) {282this.logService.trace(`ChatService#${method}: ${message}`);283} else {284this.logService.trace(`ChatService#${method}`);285}286}287288private info(method: string, message?: string): void {289if (message) {290this.logService.info(`ChatService#${method}: ${message}`);291} else {292this.logService.info(`ChatService#${method}`);293}294}295296private error(method: string, message: string): void {297this.logService.error(`ChatService#${method} ${message}`);298}299300private deserializeChats(sessionData: string): ISerializableChatsData {301try {302const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data303if (!Array.isArray(arrayOfSessions)) {304throw new Error('Expected array');305}306307const sessions = arrayOfSessions.reduce<ISerializableChatsData>((acc, session) => {308// Revive serialized markdown strings in response data309for (const request of session.requests) {310if (Array.isArray(request.response)) {311request.response = request.response.map((response) => {312if (typeof response === 'string') {313return new MarkdownString(response);314}315return response;316});317} else if (typeof request.response === 'string') {318request.response = [new MarkdownString(request.response)];319}320}321322acc[session.sessionId] = normalizeSerializableChatData(session);323return acc;324}, {});325return sessions;326} catch (err) {327this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);328return {};329}330}331332/**333* todo@connor4312 This will be cleaned up with the globalization of edits.334*/335private async reviveSessionsWithEdits(): Promise<void> {336const idx = await this._chatSessionStore.getIndex();337await Promise.all(Object.values(idx).map(async session => {338if (!session.hasPendingEdits) {339return;340}341342const sessionResource = LocalChatSessionUri.forSession(session.sessionId);343const sessionRef = await this.getOrRestoreSession(sessionResource);344if (sessionRef?.object.editingSession) {345await chatEditingSessionIsReady(sessionRef.object.editingSession);346// the session will hold a self-reference as long as there are modified files347sessionRef.dispose();348}349}));350}351352/**353* Returns an array of chat details for all persisted chat sessions that have at least one request.354* Chat sessions that have already been loaded into the chat view are excluded from the result.355* Imported chat sessions are also excluded from the result.356* TODO this is only used by the old "show chats" command which can be removed when the pre-agents view357* options are removed.358*/359async getLocalSessionHistory(): Promise<IChatDetail[]> {360const liveSessionItems = await this.getLiveSessionItems();361const historySessionItems = await this.getHistorySessionItems();362363return [...liveSessionItems, ...historySessionItems];364}365366/**367* Returns an array of chat details for all local live chat sessions.368*/369async getLiveSessionItems(): Promise<IChatDetail[]> {370return await Promise.all(Array.from(this._sessionModels.values())371.filter(session => this.shouldBeInHistory(session))372.map(async (session): Promise<IChatDetail> => {373const title = session.title || localize('newChat', "New Chat");374return {375sessionResource: session.sessionResource,376title,377lastMessageDate: session.lastMessageDate,378timing: session.timing,379isActive: true,380stats: await awaitStatsForSession(session),381lastResponseState: session.lastRequest?.response?.state ?? ResponseModelState.Pending,382};383}));384}385386/**387* Returns an array of chat details for all local chat sessions in history (not currently loaded).388*/389async getHistorySessionItems(): Promise<IChatDetail[]> {390const index = await this._chatSessionStore.getIndex();391return Object.values(index)392.filter(entry => !entry.isExternal)393.filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty)394.map((entry): IChatDetail => {395const sessionResource = LocalChatSessionUri.forSession(entry.sessionId);396return ({397...entry,398sessionResource,399isActive: this._sessionModels.has(sessionResource),400});401});402}403404async getMetadataForSession(sessionResource: URI): Promise<IChatDetail | undefined> {405const index = await this._chatSessionStore.getIndex();406const metadata: IChatSessionEntryMetadata | undefined = index[sessionResource.toString()];407if (metadata) {408return {409...metadata,410sessionResource,411isActive: this._sessionModels.has(sessionResource),412};413}414415return undefined;416}417418private shouldBeInHistory(entry: ChatModel): boolean {419return !entry.isImported && !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation === ChatAgentLocation.Chat;420}421422async removeHistoryEntry(sessionResource: URI): Promise<void> {423await this._chatSessionStore.deleteSession(this.toLocalSessionId(sessionResource));424this._onDidDisposeSession.fire({ sessionResource: [sessionResource], reason: 'cleared' });425}426427async clearAllHistoryEntries(): Promise<void> {428await this._chatSessionStore.clearAllSessions();429}430431startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference {432this.trace('startSession');433const sessionId = generateUuid();434const sessionResource = LocalChatSessionUri.forSession(sessionId);435return this._sessionModels.acquireOrCreate({436initialData: undefined,437location,438sessionResource,439sessionId,440canUseTools: options?.canUseTools ?? true,441disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive442});443}444445private _startSession(props: IStartSessionProps): ChatModel {446const { initialData, location, sessionResource, sessionId, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props;447const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, sessionId, disableBackgroundKeepAlive, inputState });448if (location === ChatAgentLocation.Chat) {449model.startEditingSession(true, transferEditingSession);450}451452this.initializeSession(model);453return model;454}455456private initializeSession(model: ChatModel): void {457this.trace('initializeSession', `Initialize session ${model.sessionResource}`);458459// Activate the default extension provided agent but do not wait460// for it to be ready so that the session can be used immediately461// without having to wait for the agent to be ready.462this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e));463}464465async activateDefaultAgent(location: ChatAgentLocation): Promise<void> {466await this.extensionService.whenInstalledExtensionsRegistered();467468const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Chat);469if (!defaultAgentData) {470throw new ErrorNoTelemetry('No default agent contributed');471}472473// Await activation of the extension provided agent474// Using `activateById` as workaround for the issue475// https://github.com/microsoft/vscode/issues/250590476if (!defaultAgentData.isCore) {477await this.extensionService.activateById(defaultAgentData.extensionId, {478activationEvent: `onChatParticipant:${defaultAgentData.id}`,479extensionId: defaultAgentData.extensionId,480startup: false481});482}483484const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id);485if (!defaultAgent) {486throw new ErrorNoTelemetry('No default agent registered');487}488}489490getSession(sessionResource: URI): IChatModel | undefined {491return this._sessionModels.get(sessionResource);492}493494getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined {495return this._sessionModels.acquireExisting(sessionResource);496}497498async getOrRestoreSession(sessionResource: URI): Promise<IChatModelReference | undefined> {499this.trace('getOrRestoreSession', `${sessionResource}`);500const existingRef = this._sessionModels.acquireExisting(sessionResource);501if (existingRef) {502return existingRef;503}504505const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);506if (!sessionId) {507throw new Error(`Cannot restore non-local session ${sessionResource}`);508}509510let sessionData: ISerializedChatDataReference | undefined;511if (isEqual(this.transferredSessionResource, sessionResource)) {512this._transferredSessionResource = undefined;513sessionData = await this._chatSessionStore.readTransferredSession(sessionResource);514} else {515sessionData = await this._chatSessionStore.readSession(sessionId);516}517518if (!sessionData) {519return undefined;520}521522const sessionRef = this._sessionModels.acquireOrCreate({523initialData: sessionData,524location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat,525sessionResource,526sessionId,527canUseTools: true,528});529530return sessionRef;531}532533// There are some cases where this returns a real string. What happens if it doesn't?534// This had titles restored from the index, so just return titles from index instead, sync.535getSessionTitle(sessionResource: URI): string | undefined {536const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);537if (!sessionId) {538return undefined;539}540541return this._sessionModels.get(sessionResource)?.title ??542this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title;543}544545loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined {546const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid();547const sessionResource = LocalChatSessionUri.forSession(sessionId);548return this._sessionModels.acquireOrCreate({549initialData: { value: data, serializer: new ChatSessionOperationLog() },550location: data.initialLocation ?? ChatAgentLocation.Chat,551sessionResource,552sessionId,553canUseTools: true,554});555}556557async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined> {558// TODO: Move this into a new ChatModelService559560if (chatSessionResource.scheme === Schemas.vscodeLocalChatSession) {561return this.getOrRestoreSession(chatSessionResource);562}563564const existingRef = this._sessionModels.acquireExisting(chatSessionResource);565if (existingRef) {566return existingRef;567}568569const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None);570const chatSessionType = chatSessionResource.scheme;571572// Contributed sessions do not use UI tools573const modelRef = this._sessionModels.acquireOrCreate({574initialData: undefined,575location,576sessionResource: chatSessionResource,577canUseTools: false,578transferEditingSession: providedSession.transferredState?.editingSession,579inputState: providedSession.transferredState?.inputState,580});581582modelRef.object.setContributedChatSession({583chatSessionResource,584chatSessionType,585isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer)586});587588const model = modelRef.object;589const disposables = new DisposableStore();590disposables.add(modelRef.object.onDidDispose(() => {591disposables.dispose();592providedSession.dispose();593}));594595let lastRequest: ChatRequestModel | undefined;596for (const message of providedSession.history) {597if (message.type === 'request') {598if (lastRequest) {599lastRequest.response?.complete();600}601602const requestText = message.prompt;603604const parsedRequest: IParsedChatRequest = {605text: requestText,606parts: [new ChatRequestTextPart(607new OffsetRange(0, requestText.length),608{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 },609requestText610)]611};612const agent =613message.participant614? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode?615: this.chatAgentService.getAgent(chatSessionType);616lastRequest = model.addRequest(parsedRequest,617message.variableData ?? { variables: [] },6180, // attempt619undefined,620agent,621undefined, // slashCommand622undefined, // confirmation623undefined, // locationData624undefined, // attachments625false, // Do not treat as requests completed, else edit pills won't show.626undefined,627undefined,628message.id629);630} else {631// response632if (lastRequest) {633for (const part of message.parts) {634model.acceptResponseProgress(lastRequest, part);635}636}637}638}639640if (providedSession.isCompleteObs?.get()) {641lastRequest?.response?.complete();642}643644if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) {645const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);646this._pendingRequests.set(model.sessionResource, initialCancellationRequest);647const cancellationListener = disposables.add(new MutableDisposable());648649const createCancellationListener = (token: CancellationToken) => {650return token.onCancellationRequested(() => {651providedSession.interruptActiveResponseCallback?.().then(userConfirmedInterruption => {652if (!userConfirmedInterruption) {653// User cancelled the interruption654const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);655this._pendingRequests.set(model.sessionResource, newCancellationRequest);656cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token);657}658});659});660};661662cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token);663664let lastProgressLength = 0;665disposables.add(autorun(reader => {666const progressArray = providedSession.progressObs?.read(reader) ?? [];667const isComplete = providedSession.isCompleteObs?.read(reader) ?? false;668669// Process only new progress items670if (progressArray.length > lastProgressLength) {671const newProgress = progressArray.slice(lastProgressLength);672for (const progress of newProgress) {673model?.acceptResponseProgress(lastRequest, progress);674}675lastProgressLength = progressArray.length;676}677678// Handle completion679if (isComplete) {680lastRequest.response?.complete();681cancellationListener.clear();682}683}));684} else {685if (lastRequest && model.editingSession) {686// wait for timeline to load so that a 'changes' part is added when the response completes687await chatEditingSessionIsReady(model.editingSession);688lastRequest.response?.complete();689}690}691692return modelRef;693}694695getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined {696const model = this._sessionModels.get(sessionResource);697if (!model) {698return;699}700const { contributedChatSession } = model;701return contributedChatSession;702}703704async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise<void> {705const model = this._sessionModels.get(request.session.sessionResource);706if (!model && model !== request.session) {707throw new Error(`Unknown session: ${request.session.sessionResource}`);708}709710const cts = this._pendingRequests.get(request.session.sessionResource);711if (cts) {712this.trace('resendRequest', `Session ${request.session.sessionResource} already has a pending request, cancelling...`);713cts.cancel();714}715716const location = options?.location ?? model.initialLocation;717const attempt = options?.attempt ?? 0;718const enableCommandDetection = !options?.noCommandDetection;719const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;720721model.removeRequest(request.id, ChatRequestRemovalReason.Resend);722723const resendOptions: IChatSendRequestOptions = {724...options,725locationData: request.locationData,726attachedContext: request.attachedContext,727};728await this._sendRequestAsync(model, model.sessionResource, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise;729}730731private queuePendingRequest(model: ChatModel, sessionResource: URI, request: string, options: IChatSendRequestOptions): ChatSendResultQueued {732const location = options.location ?? model.initialLocation;733const parsedRequest = this.parseChatRequest(sessionResource, request, location, options);734const requestModel = new ChatRequestModel({735session: model,736message: parsedRequest,737variableData: { variables: options.attachedContext ?? [] },738timestamp: Date.now(),739modeInfo: options.modeInfo,740locationData: options.locationData,741attachedContext: options.attachedContext,742modelId: options.userSelectedModelId,743userSelectedTools: options.userSelectedTools?.get(),744});745746const deferred = new DeferredPromise<ChatSendResult>();747this._queuedRequestDeferreds.set(requestModel.id, deferred);748749model.addPendingRequest(requestModel, options.queue ?? ChatRequestQueueKind.Queued, { ...options, queue: undefined });750751if (options.queue === ChatRequestQueueKind.Steering) {752this.setYieldRequested(sessionResource);753}754755this.trace('sendRequest', `Queued message for session ${sessionResource}`);756return { kind: 'queued', deferred: deferred.p };757}758759async sendRequest(sessionResource: URI, request: string, options?: IChatSendRequestOptions): Promise<ChatSendResult> {760this.trace('sendRequest', `sessionResource: ${sessionResource.toString()}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);761762763if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) {764this.trace('sendRequest', 'Rejected empty message');765return { kind: 'rejected', reason: 'Empty message' };766}767768const model = this._sessionModels.get(sessionResource);769if (!model) {770throw new Error(`Unknown session: ${sessionResource}`);771}772773const hasPendingRequest = this._pendingRequests.has(sessionResource);774const hasPendingQueue = model.getPendingRequests().length > 0;775776if (options?.queue) {777return this.queuePendingRequest(model, sessionResource, request, options);778} else if (hasPendingRequest) {779this.trace('sendRequest', `Session ${sessionResource} already has a pending request`);780return { kind: 'rejected', reason: 'Request already in progress' };781}782783if (options?.queue && hasPendingQueue) {784const queued = this.queuePendingRequest(model, sessionResource, request, options);785this.processNextPendingRequest(model);786return queued;787}788789const requests = model.getRequests();790for (let i = requests.length - 1; i >= 0; i -= 1) {791const request = requests[i];792if (request.shouldBeRemovedOnSend) {793if (request.shouldBeRemovedOnSend.afterUndoStop) {794request.response?.finalizeUndoState();795} else {796await this.removeRequest(sessionResource, request.id);797}798}799}800801const location = options?.location ?? model.initialLocation;802const attempt = options?.attempt ?? 0;803const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;804805const parsedRequest = this.parseChatRequest(sessionResource, request, location, options);806const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined;807const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;808const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);809810// This method is only returning whether the request was accepted - don't block on the actual request811return {812kind: 'sent',813data: {814...this._sendRequestAsync(model, sessionResource, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options),815agent,816slashCommand: agentSlashCommandPart?.command,817},818};819}820821private parseChatRequest(sessionResource: URI, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest {822let parserContext = options?.parserContext;823if (options?.agentId) {824const agent = this.chatAgentService.getAgent(options.agentId);825if (!agent) {826throw new Error(`Unknown agent: ${options.agentId}`);827}828parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind };829const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : '';830request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`;831}832833const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, request, location, parserContext);834return parsedRequest;835}836837private refreshFollowupsCancellationToken(sessionResource: URI): CancellationToken {838this._sessionFollowupCancelTokens.get(sessionResource)?.cancel();839const newTokenSource = new CancellationTokenSource();840this._sessionFollowupCancelTokens.set(sessionResource, newTokenSource);841842return newTokenSource.token;843}844845private _sendRequestAsync(model: ChatModel, sessionResource: URI, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {846const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionResource);847let request: ChatRequestModel;848const agentPart = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);849const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);850const commandPart = parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);851const requests = [...model.getRequests()];852const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, {853agent: agentPart?.agent ?? defaultAgent,854agentSlashCommandPart,855commandPart,856sessionId: model.sessionId,857location: model.initialLocation,858options,859enableCommandDetection860});861862let gotProgress = false;863const requestType = commandPart ? 'slashCommand' : 'string';864865const responseCreated = new DeferredPromise<IChatResponseModel>();866let responseCreatedComplete = false;867function completeResponseCreated(): void {868if (!responseCreatedComplete && request?.response) {869responseCreated.complete(request.response);870responseCreatedComplete = true;871}872}873874const store = new DisposableStore();875const source = store.add(new CancellationTokenSource());876const token = source.token;877const sendRequestInternal = async () => {878const progressCallback = (progress: IChatProgress[]) => {879if (token.isCancellationRequested) {880return;881}882883gotProgress = true;884885for (let i = 0; i < progress.length; i++) {886const isLast = i === progress.length - 1;887const progressItem = progress[i];888889if (progressItem.kind === 'markdownContent') {890this.trace('sendRequest', `Provider returned progress for session ${model.sessionResource}, ${progressItem.content.value.length} chars`);891} else {892this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`);893}894895model.acceptResponseProgress(request, progressItem, !isLast);896}897completeResponseCreated();898};899900let detectedAgent: IChatAgentData | undefined;901let detectedCommand: IChatAgentCommand | undefined;902903// Collect hooks from hook .json files904let collectedHooks: IChatRequestHooks | undefined;905try {906collectedHooks = await this.promptsService.getHooks(token);907} catch (error) {908this.logService.warn('[ChatService] Failed to collect hooks:', error);909}910911const stopWatch = new StopWatch(false);912store.add(token.onCancellationRequested(() => {913this.trace('sendRequest', `Request for session ${model.sessionResource} was cancelled`);914if (!request) {915return;916}917918requestTelemetry.complete({919timeToFirstProgress: undefined,920result: 'cancelled',921// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling922totalTime: stopWatch.elapsed(),923requestType,924detectedAgent,925request,926});927928model.cancelRequest(request);929}));930931try {932let rawResult: IChatAgentResult | null | undefined;933let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;934if (agentPart || (defaultAgent && !commandPart)) {935const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => {936const initVariableData: IChatRequestVariableData = { variables: [] };937request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId, options?.userSelectedTools?.get());938939let variableData: IChatRequestVariableData;940let message: string;941if (chatRequest) {942variableData = chatRequest.variableData;943message = getPromptText(request.message).message;944} else {945variableData = { variables: this.prepareContext(request.attachedContext) };946model.updateRequest(request, variableData);947948// Merge resolved variables (e.g. images from directories) for the949// agent request only - they are not stored on the request model.950if (options?.resolvedVariables?.length) {951variableData = { variables: [...variableData.variables, ...options.resolvedVariables] };952}953954const promptTextResult = getPromptText(request.message);955variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack956message = promptTextResult.message;957}958959const agentRequest: IChatAgentRequest = {960sessionResource: model.sessionResource,961requestId: request.id,962agentId: agent.id,963message,964command: command?.name,965variables: variableData,966enableCommandDetection,967isParticipantDetected,968attempt,969location,970locationData: request.locationData,971acceptedConfirmationData: options?.acceptedConfirmationData,972rejectedConfirmationData: options?.rejectedConfirmationData,973userSelectedModelId: options?.userSelectedModelId,974userSelectedTools: options?.userSelectedTools?.get(),975modeInstructions: options?.modeInfo?.modeInstructions,976editedFileEvents: request.editedFileEvents,977hooks: collectedHooks,978hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0),979};980981let isInitialTools = true;982983store.add(autorun(reader => {984const tools = options?.userSelectedTools?.read(reader);985if (isInitialTools) {986isInitialTools = false;987return;988}989990if (tools) {991this.chatAgentService.setRequestTools(agent.id, request.id, tools);992// in case the request has not been sent out yet:993agentRequest.userSelectedTools = tools;994}995}));996997return agentRequest;998};9991000if (1001this.configurationService.getValue('chat.detectParticipant.enabled') !== false &&1002this.chatAgentService.hasChatParticipantDetectionProviders() &&1003!agentPart &&1004!commandPart &&1005!agentSlashCommandPart &&1006enableCommandDetection &&1007(location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) &&1008options?.modeInfo?.kind !== ChatModeKind.Agent &&1009options?.modeInfo?.kind !== ChatModeKind.Edit &&1010!options?.agentIdSilent1011) {1012// We have no agent or command to scope history with, pass the full history to the participant detection provider1013const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, location, defaultAgent.id);10141015// Prepare the request object that we will send to the participant detection provider1016const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false);10171018const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token);1019if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) {1020// Update the response in the ChatModel to reflect the detected agent and command1021request.response?.setAgent(result.agent, result.command);1022detectedAgent = result.agent;1023detectedCommand = result.command;1024}1025}10261027const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!;1028const command = detectedCommand ?? agentSlashCommandPart?.command;10291030await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`);10311032// Recompute history in case the agent or command changed1033const history = this.getHistoryEntriesFromModel(requests, location, agent.id);1034const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent);1035this.generateInitialChatTitleIfNeeded(model, requestProps, defaultAgent, token);1036const pendingRequest = this._pendingRequests.get(sessionResource);1037if (pendingRequest) {1038store.add(autorun(reader => {1039if (pendingRequest.yieldRequested.read(reader)) {1040this.chatAgentService.setYieldRequested(agent.id, request.id);1041}1042}));1043pendingRequest.requestId ??= requestProps.requestId;1044}1045completeResponseCreated();10461047// MCP autostart: only run for native VS Code sessions (sidebar, new editors) but not for extension contributed sessions that have inputType set.1048if (model.canUseTools) {1049const autostartResult = new ChatMcpServersStarting(this.mcpService.autostart(token));1050if (!autostartResult.isEmpty) {1051progressCallback([autostartResult]);1052await autostartResult.wait();1053}1054}10551056const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);1057rawResult = agentResult;1058agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);1059} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {1060if (commandPart.slashCommand.silent !== true) {1061request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo);1062completeResponseCreated();1063}1064// contributed slash commands1065// TODO: spell this out in the UI1066const history: IChatMessage[] = [];1067for (const modelRequest of model.getRequests()) {1068if (!modelRequest.response) {1069continue;1070}1071history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] });1072history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] });1073}1074const message = parsedRequest.text;1075const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {1076progressCallback([p]);1077}), history, location, model.sessionResource, token);1078agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);1079rawResult = {};10801081} else {1082throw new Error(`Cannot handle request`);1083}10841085if (token.isCancellationRequested && !rawResult) {1086return;1087} else {1088if (!rawResult) {1089this.trace('sendRequest', `Provider returned no response for session ${model.sessionResource}`);1090rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };1091}10921093const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' :1094rawResult.errorDetails && gotProgress ? 'errorWithOutput' :1095rawResult.errorDetails ? 'error' :1096'success';10971098requestTelemetry.complete({1099timeToFirstProgress: rawResult.timings?.firstProgress,1100totalTime: rawResult.timings?.totalElapsed,1101result,1102requestType,1103detectedAgent,1104request,1105});11061107model.setResponse(request, rawResult);1108completeResponseCreated();1109this.trace('sendRequest', `Provider returned response for session ${model.sessionResource}`);11101111shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested;1112request.response?.complete();1113if (agentOrCommandFollowups) {1114agentOrCommandFollowups.then(followups => {1115model.setFollowups(request, followups);1116const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command;1117this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0);1118});1119}1120}1121} catch (err) {1122this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`);1123requestTelemetry.complete({1124timeToFirstProgress: undefined,1125totalTime: undefined,1126result: 'error',1127requestType,1128detectedAgent,1129request,1130});1131if (request) {1132const rawResult: IChatAgentResult = { errorDetails: { message: err.message } };1133model.setResponse(request, rawResult);1134completeResponseCreated();1135request.response?.complete();1136}1137} finally {1138store.dispose();1139}1140};1141let shouldProcessPending = false;1142const rawResponsePromise = sendRequestInternal();1143// Note- requestId is not known at this point, assigned later1144this._pendingRequests.set(model.sessionResource, this.instantiationService.createInstance(CancellableRequest, source, undefined));1145rawResponsePromise.finally(() => {1146this._pendingRequests.deleteAndDispose(model.sessionResource);1147// Process the next pending request from the queue if any1148if (shouldProcessPending) {1149this.processNextPendingRequest(model);1150}1151});1152this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource });1153return {1154responseCreatedPromise: responseCreated.p,1155responseCompletePromise: rawResponsePromise,1156};1157}11581159processPendingRequests(sessionResource: URI): void {1160const model = this._sessionModels.get(sessionResource);1161if (model && !this._pendingRequests.has(sessionResource)) {1162this.processNextPendingRequest(model);1163}1164}11651166/**1167* Process the next pending request from the model's queue, if any.1168* Called after a request completes to continue processing queued requests.1169*/1170private processNextPendingRequest(model: ChatModel): void {1171const pendingRequest = model.dequeuePendingRequest();1172if (!pendingRequest) {1173return;1174}11751176this.trace('processNextPendingRequest', `Processing queued request for session ${model.sessionResource}`);11771178const deferred = this._queuedRequestDeferreds.get(pendingRequest.request.id);1179this._queuedRequestDeferreds.delete(pendingRequest.request.id);11801181const sendOptions: IChatSendRequestOptions = {1182...pendingRequest.sendOptions,1183// Ensure attachedContext is preserved after deserialization, where sendOptions1184// loses attachedContext but the request model retains it in variableData.1185attachedContext: pendingRequest.request.variableData.variables.slice(),1186};1187const location = sendOptions.location ?? sendOptions.locationData?.type ?? model.initialLocation;1188const defaultAgent = this.chatAgentService.getDefaultAgent(location, sendOptions.modeInfo?.kind);1189if (!defaultAgent) {1190this.logService.warn('processNextPendingRequest', `No default agent for location ${location}`);1191deferred?.complete({ kind: 'rejected', reason: 'No default agent available' });1192return;1193}11941195const parsedRequest = pendingRequest.request.message;1196const silentAgent = sendOptions.agentIdSilent ? this.chatAgentService.getAgent(sendOptions.agentIdSilent) : undefined;1197const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;1198const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);11991200// Send the queued request - this will add it to _pendingRequests and handle it normally1201const responseState = this._sendRequestAsync(model, model.sessionResource, parsedRequest, pendingRequest.request.attempt, !sendOptions.noCommandDetection, silentAgent ?? defaultAgent, location, sendOptions);12021203// Resolve the deferred with the sent result1204deferred?.complete({1205kind: 'sent',1206data: {1207...responseState,1208agent,1209slashCommand: agentSlashCommandPart?.command,1210},1211});1212}12131214private generateInitialChatTitleIfNeeded(model: ChatModel, request: IChatAgentRequest, defaultAgent: IChatAgentData, token: CancellationToken): void {1215// Generate a title only for the first request, and only via the default agent.1216// Use a single-entry history based on the current request (no full chat history).1217if (model.getRequests().length !== 1 || model.customTitle) {1218return;1219}12201221const singleEntryHistory: IChatAgentHistoryEntry[] = [{1222request,1223response: [],1224result: {}1225}];1226const generate = async () => {1227const title = await this.chatAgentService.getChatTitle(defaultAgent.id, singleEntryHistory, token);1228if (title && !model.customTitle) {1229model.setCustomTitle(title);1230}1231};1232void generate();1233}12341235private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] {1236attachedContextVariables ??= [];12371238// "reverse", high index first so that replacement is simple1239attachedContextVariables.sort((a, b) => {1240// If either range is undefined, sort it to the back1241if (!a.range && !b.range) {1242return 0; // Keep relative order if both ranges are undefined1243}1244if (!a.range) {1245return 1; // a goes after b1246}1247if (!b.range) {1248return -1; // a goes before b1249}1250return b.range.start - a.range.start;1251});12521253return attachedContextVariables;1254}12551256private getHistoryEntriesFromModel(requests: IChatRequestModel[], location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] {1257const history: IChatAgentHistoryEntry[] = [];1258const agent = this.chatAgentService.getAgent(forAgentId);1259for (const request of requests) {1260if (!request.response) {1261continue;1262}12631264if (forAgentId !== request.response.agent?.id && !agent?.isDefault && !agent?.canAccessPreviousChatHistory) {1265// An agent only gets to see requests that were sent to this agent.1266// The default agent (the undefined case), or agents with 'canAccessPreviousChatHistory', get to see all of them.1267continue;1268}12691270// Do not save to history inline completions1271if (location === ChatAgentLocation.EditorInline) {1272continue;1273}12741275const promptTextResult = getPromptText(request.message);1276const historyRequest: IChatAgentRequest = {1277sessionResource: request.session.sessionResource,1278requestId: request.id,1279agentId: request.response.agent?.id ?? '',1280message: promptTextResult.message,1281command: request.response.slashCommand?.name,1282variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack1283location: ChatAgentLocation.Chat,1284editedFileEvents: request.editedFileEvents,1285};1286history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} });1287}12881289return history;1290}12911292async removeRequest(sessionResource: URI, requestId: string): Promise<void> {1293const model = this._sessionModels.get(sessionResource);1294if (!model) {1295throw new Error(`Unknown session: ${sessionResource}`);1296}12971298const pendingRequest = this._pendingRequests.get(sessionResource);1299if (pendingRequest?.requestId === requestId) {1300pendingRequest.cancel();1301this._pendingRequests.deleteAndDispose(sessionResource);1302}13031304model.removeRequest(requestId);1305}13061307async adoptRequest(sessionResource: URI, request: IChatRequestModel) {1308if (!(request instanceof ChatRequestModel)) {1309throw new TypeError('Can only adopt requests of type ChatRequestModel');1310}1311const target = this._sessionModels.get(sessionResource);1312if (!target) {1313throw new Error(`Unknown session: ${sessionResource}`);1314}13151316const oldOwner = request.session;1317target.adoptRequest(request);13181319if (request.response && !request.response.isComplete) {1320const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionResource);1321if (cts) {1322cts.requestId = request.id;1323this._pendingRequests.set(target.sessionResource, cts);1324}1325}1326}13271328async addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise<void> {1329this.trace('addCompleteRequest', `message: ${message}`);13301331const model = this._sessionModels.get(sessionResource);1332if (!model) {1333throw new Error(`Unknown session: ${sessionResource}`);1334}13351336const parsedRequest = typeof message === 'string' ?1337this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, message) :1338message;1339const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true);1340if (typeof response.message === 'string') {1341// TODO is this possible?1342model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' });1343} else {1344for (const part of response.message) {1345model.acceptResponseProgress(request, part, true);1346}1347}1348model.setResponse(request, response.result || {});1349if (response.followups !== undefined) {1350model.setFollowups(request, response.followups);1351}1352request.response?.complete();1353}13541355cancelCurrentRequestForSession(sessionResource: URI): void {1356this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`);1357this._pendingRequests.get(sessionResource)?.cancel();1358this._pendingRequests.deleteAndDispose(sessionResource);1359}13601361setYieldRequested(sessionResource: URI): void {1362const pendingRequest = this._pendingRequests.get(sessionResource);1363if (pendingRequest) {1364pendingRequest.setYieldRequested();1365}1366}13671368removePendingRequest(sessionResource: URI, requestId: string): void {1369const model = this._sessionModels.get(sessionResource) as ChatModel | undefined;1370if (model) {1371model.removePendingRequest(requestId);1372}13731374// Reject the deferred promise for the removed request1375const deferred = this._queuedRequestDeferreds.get(requestId);1376if (deferred) {1377deferred.complete({ kind: 'rejected', reason: 'Request was removed from queue' });1378this._queuedRequestDeferreds.delete(requestId);1379}1380}13811382setPendingRequests(sessionResource: URI, requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void {1383const model = this._sessionModels.get(sessionResource) as ChatModel | undefined;1384if (model) {1385model.setPendingRequests(requests);1386}1387}13881389public hasSessions(): boolean {1390return this._chatSessionStore.hasSessions();1391}13921393async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise<void> {1394if (!LocalChatSessionUri.isLocalSession(transferredSessionResource)) {1395throw new Error(`Can only transfer local chat sessions. Invalid session: ${transferredSessionResource}`);1396}13971398const model = this._sessionModels.get(transferredSessionResource) as ChatModel | undefined;1399if (!model) {1400throw new Error(`Failed to transfer session. Unknown session: ${transferredSessionResource}`);1401}14021403if (model.initialLocation !== ChatAgentLocation.Chat) {1404throw new Error(`Can only transfer chat sessions located in the Chat view. Session ${transferredSessionResource} has location=${model.initialLocation}`);1405}14061407await this._chatSessionStore.storeTransferSession({1408sessionResource: model.sessionResource,1409timestampInMilliseconds: Date.now(),1410toWorkspace: toWorkspace,1411}, model);1412this.chatTransferService.addWorkspaceToTransferred(toWorkspace);1413this.trace('transferChatSession', `Transferred session ${model.sessionResource} to workspace ${toWorkspace.toString()}`);1414}14151416getChatStorageFolder(): URI {1417return this._chatSessionStore.getChatStorageFolder();1418}14191420logChatIndex(): void {1421this._chatSessionStore.logIndex();1422}14231424setTitle(sessionResource: URI, title: string): void {1425this._sessionModels.get(sessionResource)?.setCustomTitle(title);1426}14271428appendProgress(request: IChatRequestModel, progress: IChatProgress): void {1429const model = this._sessionModels.get(request.session.sessionResource);1430if (!(request instanceof ChatRequestModel)) {1431throw new BugIndicatingError('Can only append progress to requests of type ChatRequestModel');1432}14331434model?.acceptResponseProgress(request, progress);1435}14361437private toLocalSessionId(sessionResource: URI) {1438const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource);1439if (!localSessionId) {1440throw new Error(`Invalid local chat session resource: ${sessionResource}`);1441}1442return localSessionId;1443}1444}144514461447