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