Path: blob/main/src/vs/workbench/api/common/extHostChatSessions.ts
3296 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 type * as vscode from 'vscode';6import { coalesce } from '../../../base/common/arrays.js';7import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';8import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';9import { MarshalledId } from '../../../base/common/marshallingIds.js';10import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';11import { ILogService } from '../../../platform/log/common/log.js';12import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';13import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js';14import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';15import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';16import { ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js';17import { ChatAgentResponseStream } from './extHostChatAgents2.js';18import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';19import { ExtHostLanguageModels } from './extHostLanguageModels.js';20import { IExtHostRpcService } from './extHostRpcService.js';21import * as typeConvert from './extHostTypeConverters.js';22import * as extHostTypes from './extHostTypes.js';23import { revive } from '../../../base/common/marshalling.js';2425class ExtHostChatSession {26private _stream: ChatAgentResponseStream;2728constructor(29public readonly session: vscode.ChatSession,30public readonly extension: IExtensionDescription,31request: IChatAgentRequest,32public readonly proxy: IChatAgentProgressShape,33public readonly commandsConverter: CommandsConverter,34public readonly sessionDisposables: DisposableStore35) {36this._stream = new ChatAgentResponseStream(extension, request, proxy, commandsConverter, sessionDisposables);37}3839get activeResponseStream() {40return this._stream;41}4243getActiveRequestStream(request: IChatAgentRequest) {44return new ChatAgentResponseStream(this.extension, request, this.proxy, this.commandsConverter, this.sessionDisposables);45}46}4748export class ExtHostChatSessions extends Disposable implements ExtHostChatSessionsShape {49private static _sessionHandlePool = 0;5051private readonly _proxy: Proxied<MainThreadChatSessionsShape>;52private readonly _chatSessionItemProviders = new Map<number, {53readonly provider: vscode.ChatSessionItemProvider;54readonly extension: IExtensionDescription;55readonly disposable: DisposableStore;56}>();57private readonly _chatSessionContentProviders = new Map<number, {58readonly provider: vscode.ChatSessionContentProvider;59readonly extension: IExtensionDescription;60readonly capabilities?: vscode.ChatSessionCapabilities;61readonly disposable: DisposableStore;62}>();63private _nextChatSessionItemProviderHandle = 0;64private _nextChatSessionContentProviderHandle = 0;65private readonly _sessionMap: Map<string, vscode.ChatSessionItem> = new Map();6667constructor(68private readonly commands: ExtHostCommands,69private readonly _languageModels: ExtHostLanguageModels,70@IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService,71@ILogService private readonly _logService: ILogService,72) {73super();74this._proxy = this._extHostRpc.getProxy(MainContext.MainThreadChatSessions);7576commands.registerArgumentProcessor({77processArgument: (arg) => {78if (arg && arg.$mid === MarshalledId.ChatSessionContext) {79const id = arg.session.id;80const sessionContent = this._sessionMap.get(id);81if (sessionContent) {82return sessionContent;83} else {84this._logService.warn(`No chat session found for ID: ${id}`);85return arg;86}87}8889return arg;90}91});92}9394registerChatSessionItemProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionItemProvider): vscode.Disposable {95const handle = this._nextChatSessionItemProviderHandle++;96const disposables = new DisposableStore();9798this._chatSessionItemProviders.set(handle, { provider, extension, disposable: disposables });99this._proxy.$registerChatSessionItemProvider(handle, chatSessionType);100if (provider.onDidChangeChatSessionItems) {101disposables.add(provider.onDidChangeChatSessionItems(() => {102this._proxy.$onDidChangeChatSessionItems(handle);103}));104}105return {106dispose: () => {107this._chatSessionItemProviders.delete(handle);108disposables.dispose();109this._proxy.$unregisterChatSessionItemProvider(handle);110}111};112}113114registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {115const handle = this._nextChatSessionContentProviderHandle++;116const disposables = new DisposableStore();117118this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables });119this._proxy.$registerChatSessionContentProvider(handle, chatSessionType);120121return new extHostTypes.Disposable(() => {122this._chatSessionContentProviders.delete(handle);123disposables.dispose();124this._proxy.$unregisterChatSessionContentProvider(handle);125});126}127128async showChatSession(_extension: IExtensionDescription, chatSessionType: string, sessionId: string, options: vscode.ChatSessionShowOptions | undefined): Promise<void> {129await this._proxy.$showChatSession(chatSessionType, sessionId, typeConvert.ViewColumn.from(options?.viewColumn));130}131132private convertChatSessionStatus(status: vscode.ChatSessionStatus | undefined): ChatSessionStatus | undefined {133if (status === undefined) {134return undefined;135}136switch (status) {137case 0: // vscode.ChatSessionStatus.Failed138return ChatSessionStatus.Failed;139case 1: // vscode.ChatSessionStatus.Completed140return ChatSessionStatus.Completed;141case 2: // vscode.ChatSessionStatus.InProgress142return ChatSessionStatus.InProgress;143default:144return undefined;145}146}147148private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {149return {150id: sessionContent.id,151label: sessionContent.label,152description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,153status: this.convertChatSessionStatus(sessionContent.status),154tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),155timing: {156startTime: sessionContent.timing?.startTime ?? 0,157endTime: sessionContent.timing?.endTime158},159statistics: sessionContent.statistics ? {160insertions: sessionContent.statistics?.insertions ?? 0,161deletions: sessionContent.statistics?.deletions ?? 0162} : undefined163};164}165166async $provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; prompt?: string; history: any[]; metadata?: any }, token: CancellationToken): Promise<IChatSessionItem> {167const entry = this._chatSessionItemProviders.get(handle);168if (!entry || !entry.provider.provideNewChatSessionItem) {169throw new Error(`No provider registered for handle ${handle} or provider does not support creating sessions`);170}171172try {173const model = await this.getModelForRequest(options.request, entry.extension);174const vscodeRequest = typeConvert.ChatAgentRequest.to(175revive(options.request),176undefined,177model,178[],179new Map(),180entry.extension,181this._logService);182183const vscodeOptions = {184request: vscodeRequest,185prompt: options.prompt,186history: options.history,187metadata: options.metadata188};189190const chatSessionItem = await entry.provider.provideNewChatSessionItem(vscodeOptions, token);191if (!chatSessionItem || !chatSessionItem.id) {192throw new Error('Provider did not create session');193}194this._sessionMap.set(195chatSessionItem.id,196chatSessionItem197);198return this.convertChatSessionItem(chatSessionItem);199} catch (error) {200this._logService.error(`Error creating chat session: ${error}`);201throw error;202}203}204205async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {206const entry = this._chatSessionItemProviders.get(handle);207if (!entry) {208this._logService.error(`No provider registered for handle ${handle}`);209return [];210}211212const sessions = await entry.provider.provideChatSessionItems(token);213if (!sessions) {214return [];215}216217const response: IChatSessionItem[] = [];218for (const sessionContent of sessions) {219if (sessionContent.id) {220this._sessionMap.set(221sessionContent.id,222sessionContent223);224response.push(this.convertChatSessionItem(sessionContent));225}226}227return response;228}229230private readonly _extHostChatSessions = new Map<string, { readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>();231232async $provideChatSessionContent(handle: number, id: string, token: CancellationToken): Promise<ChatSessionDto> {233const provider = this._chatSessionContentProviders.get(handle);234if (!provider) {235throw new Error(`No provider for handle ${handle}`);236}237238const session = await provider.provider.provideChatSessionContent(id, token);239240const sessionDisposables = new DisposableStore();241const sessionId = ExtHostChatSessions._sessionHandlePool++;242const chatSession = new ExtHostChatSession(session, provider.extension, {243sessionId: `${id}.${sessionId}`,244requestId: 'ongoing',245agentId: id,246message: '',247variables: { variables: [] },248location: ChatAgentLocation.Panel,249}, {250$handleProgressChunk: (requestId, chunks) => {251return this._proxy.$handleProgressChunk(handle, id, requestId, chunks);252},253$handleAnchorResolve: (requestId, requestHandle, anchor) => {254this._proxy.$handleAnchorResolve(handle, id, requestId, requestHandle, anchor);255},256}, this.commands.converter, sessionDisposables);257258const disposeCts = sessionDisposables.add(new CancellationTokenSource());259this._extHostChatSessions.set(`${handle}_${id}`, { sessionObj: chatSession, disposeCts });260261// Call activeResponseCallback immediately for best user experience262if (session.activeResponseCallback) {263Promise.resolve(session.activeResponseCallback(chatSession.activeResponseStream.apiObject, disposeCts.token)).finally(() => {264// complete265this._proxy.$handleProgressComplete(handle, id, 'ongoing');266});267}268const { capabilities } = provider;269return {270id: sessionId + '',271hasActiveResponseCallback: !!session.activeResponseCallback,272hasRequestHandler: !!session.requestHandler,273supportsInterruption: !!capabilities?.supportsInterruptions,274history: session.history.map(turn => {275if (turn instanceof extHostTypes.ChatRequestTurn) {276return { type: 'request' as const, prompt: turn.prompt, participant: turn.participant };277} else {278const responseTurn = turn as extHostTypes.ChatResponseTurn2;279const parts = coalesce(responseTurn.response.map(r => typeConvert.ChatResponsePart.from(r, this.commands.converter, sessionDisposables)));280281return {282type: 'response' as const,283parts,284participant: responseTurn.participant285};286}287})288};289}290291async $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise<void> {292const key = `${providerHandle}_${sessionId}`;293const entry = this._extHostChatSessions.get(key);294entry?.disposeCts.cancel();295}296297async $disposeChatSessionContent(providerHandle: number, sessionId: string): Promise<void> {298const key = `${providerHandle}_${sessionId}`;299const entry = this._extHostChatSessions.get(key);300if (!entry) {301this._logService.warn(`No chat session found for ID: ${key}`);302return;303}304305entry.disposeCts.cancel();306entry.sessionObj.sessionDisposables.dispose();307this._extHostChatSessions.delete(key);308}309310async $invokeChatSessionRequestHandler(handle: number, id: string, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult> {311const entry = this._extHostChatSessions.get(`${handle}_${id}`);312if (!entry || !entry.sessionObj.session.requestHandler) {313return {};314}315316const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), [], new Map(), entry.sessionObj.extension, this._logService);317318const stream = entry.sessionObj.getActiveRequestStream(request);319await entry.sessionObj.session.requestHandler(chatRequest, { history: history }, stream.apiObject, token);320321// TODO: do we need to dispose the stream object?322return {};323}324325private async getModelForRequest(request: IChatAgentRequest, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {326let model: vscode.LanguageModelChat | undefined;327if (request.userSelectedModelId) {328model = await this._languageModels.getLanguageModelByIdentifier(extension, request.userSelectedModelId);329}330if (!model) {331model = await this._languageModels.getDefaultLanguageModel(extension);332if (!model) {333throw new Error('Language model unavailable');334}335}336337return model;338}339}340341342