Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts
5241 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 { assertNever } from '../../../../../base/common/assert.js';6import { isMarkdownString } from '../../../../../base/common/htmlContent.js';7import { equals as objectsEqual } from '../../../../../base/common/objects.js';8import { isEqual as _urisEqual } from '../../../../../base/common/resources.js';9import { hasKey } from '../../../../../base/common/types.js';10import { URI, UriComponents } from '../../../../../base/common/uri.js';11import { IChatMarkdownContent, ResponseModelState } from '../chatService/chatService.js';12import { ModifiedFileEntryState } from '../editing/chatEditingService.js';13import { IParsedChatRequest } from '../requestParser/chatParserTypes.js';14import { IChatAgentEditedFileEvent, IChatDataSerializerLog, IChatModel, IChatPendingRequest, IChatProgressResponseContent, IChatRequestModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatModelInputState, ISerializableChatRequestData, ISerializablePendingRequestData, SerializedChatResponsePart, serializeSendOptions } from './chatModel.js';15import * as Adapt from './objectMutationLog.js';1617/**18* ChatModel has lots of properties and lots of ways those properties can mutate.19* The naive way to store the ChatModel is serializing it to JSON and calling it20* a day. However, chats can get very, very long, and thus doing so is slow.21*22* In this file, we define a `storageSchema` that adapters from the `IChatModel`23* into the serializable format. This schema tells us what properties in the chat24* model correspond to the serialized properties, *and how they change*. For25* example, `Adapt.constant(...)` defines a property that will never be checked26* for changes after it's written, and `Adapt.primitive(...)` defines a property27* that will be checked for changes using strict equality each time we store it.28*29* We can then use this to generate a log of mutations that we can append to30* cheaply without rewriting and reserializing the entire request each time.31*/3233const toJson = <T>(obj: T): T extends { toJSON?(): infer R } ? R : T => {34const cast = obj as { toJSON?: () => T };35// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any36return (cast && typeof cast.toJSON === 'function' ? cast.toJSON() : obj) as any;37};3839const responsePartSchema = Adapt.v<IChatProgressResponseContent, SerializedChatResponsePart>(40(obj): SerializedChatResponsePart => obj.kind === 'markdownContent' ? obj.content : toJson(obj),41(a, b) => {42if (isMarkdownString(a) && isMarkdownString(b)) {43return a.value === b.value;44}4546if (hasKey(a, { kind: true }) && hasKey(b, { kind: true })) {47if (a.kind !== b.kind) {48return false;49}5051switch (a.kind) {52case 'markdownContent':53return a.content === (b as IChatMarkdownContent).content;5455// Dynamic types that can change after initial push need deep equality56// Note: these are the *serialized* kind names (e.g. toolInvocationSerialized not toolInvocation)57case 'toolInvocationSerialized':58case 'elicitationSerialized':59case 'progressTaskSerialized':60case 'textEditGroup':61case 'multiDiffData':62case 'mcpServersStarting':63return objectsEqual(a, b);6465// Static types that won't change after being pushed can use strict equality.66case 'clearToPreviousToolInvocation':67case 'codeblockUri':68case 'command':69case 'confirmation':70case 'extensions':71case 'hook':72case 'inlineReference':73case 'markdownVuln':74case 'notebookEditGroup':75case 'progressMessage':76case 'pullRequest':77case 'questionCarousel':78case 'thinking':79case 'undoStop':80case 'warning':81case 'treeData':82case 'workspaceEdit':83return a.kind === b.kind;8485default: {86// Hello developer! You are probably here because you added a new chat response type.87// This logic controls when we'll update chat parts stored on disk as part of the session.88// If it's a 'static' type that is not expected to change, add it to the 'return true'89// block above. However it's a type that is going to change, add it to the 'objectsEqual'90// block or make something more tailored.91assertNever(a);92}93}94}9596return false;97}98);99100const urisEqual = (a: UriComponents, b: UriComponents): boolean => {101return _urisEqual(URI.from(a), URI.from(b));102};103104const messageSchema = Adapt.object<IParsedChatRequest, IParsedChatRequest>({105text: Adapt.v(m => m.text),106parts: Adapt.v(m => m.parts, (a, b) => a.length === b.length && a.every((part, i) => part.text === b[i].text)),107});108109const agentEditedFileEventSchema = Adapt.object<IChatAgentEditedFileEvent, IChatAgentEditedFileEvent>({110uri: Adapt.v(e => e.uri, urisEqual),111eventKind: Adapt.v(e => e.eventKind),112});113114const chatVariableSchema = Adapt.object<IChatRequestVariableData, IChatRequestVariableData>({115variables: Adapt.t(v => v.variables, Adapt.array(Adapt.value((a, b) => a.name === b.name))),116});117118const requestSchema = Adapt.object<IChatRequestModel, ISerializableChatRequestData>({119// request parts120requestId: Adapt.t(m => m.id, Adapt.key()),121timestamp: Adapt.v(m => m.timestamp),122confirmation: Adapt.v(m => m.confirmation),123message: Adapt.t(m => m.message, messageSchema),124shouldBeRemovedOnSend: Adapt.v(m => m.shouldBeRemovedOnSend, objectsEqual),125agent: Adapt.v(m => m.response?.agent, (a, b) => a?.id === b?.id),126modelId: Adapt.v(m => m.modelId),127editedFileEvents: Adapt.t(m => m.editedFileEvents, Adapt.array(agentEditedFileEventSchema)),128variableData: Adapt.t(m => m.variableData, chatVariableSchema),129isHidden: Adapt.v(() => undefined), // deprecated, always undefined for new data130isCanceled: Adapt.v(() => undefined), // deprecated, modelState is used instead131132// response parts (from ISerializableChatResponseData via response.toJSON())133response: Adapt.t(m => m.response?.entireResponse.value, Adapt.array(responsePartSchema)),134responseId: Adapt.v(m => m.response?.id),135result: Adapt.v(m => m.response?.result, objectsEqual),136responseMarkdownInfo: Adapt.v(137m => m.response?.codeBlockInfos?.map(info => ({ suggestionId: info.suggestionId })),138objectsEqual,139),140followups: Adapt.v(m => m.response?.followups, objectsEqual),141modelState: Adapt.v(m => m.response?.stateT, objectsEqual),142vote: Adapt.v(m => m.response?.vote),143voteDownReason: Adapt.v(m => m.response?.voteDownReason),144slashCommand: Adapt.t(m => m.response?.slashCommand, Adapt.value((a, b) => a?.name === b?.name)),145usedContext: Adapt.v(m => m.response?.usedContext, objectsEqual),146contentReferences: Adapt.v(m => m.response?.contentReferences, objectsEqual),147codeCitations: Adapt.v(m => m.response?.codeCitations, objectsEqual),148timeSpentWaiting: Adapt.v(m => m.response?.timestamp), // based on response timestamp149}, {150sealed: (o) => o.modelState?.value === ResponseModelState.Cancelled || o.modelState?.value === ResponseModelState.Failed || o.modelState?.value === ResponseModelState.Complete,151});152153const inputStateSchema = Adapt.object<ISerializableChatModelInputState, ISerializableChatModelInputState>({154attachments: Adapt.v(i => i.attachments, objectsEqual),155mode: Adapt.v(i => i.mode, (a, b) => a.id === b.id),156selectedModel: Adapt.v(i => i.selectedModel, (a, b) => a?.identifier === b?.identifier),157inputText: Adapt.v(i => i.inputText),158selections: Adapt.v(i => i.selections, objectsEqual),159contrib: Adapt.v(i => i.contrib, objectsEqual),160});161162const pendingRequestSchema = Adapt.object<IChatPendingRequest, ISerializablePendingRequestData>({163id: Adapt.t(p => p.request.id, Adapt.key()),164request: Adapt.t(p => p.request, requestSchema),165kind: Adapt.v(p => p.kind),166sendOptions: Adapt.v(p => serializeSendOptions(p.sendOptions), objectsEqual),167});168169export const storageSchema = Adapt.object<IChatModel, ISerializableChatData>({170version: Adapt.v(() => 3),171creationDate: Adapt.v(m => m.timestamp),172customTitle: Adapt.v(m => m.hasCustomTitle ? m.title : undefined),173initialLocation: Adapt.v(m => m.initialLocation),174inputState: Adapt.t(m => m.inputModel.toJSON(), inputStateSchema),175responderUsername: Adapt.v(m => m.responderUsername),176sessionId: Adapt.v(m => m.sessionId),177requests: Adapt.t(m => m.getRequests(), Adapt.array(requestSchema)),178hasPendingEdits: Adapt.v(m => m.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)),179repoData: Adapt.v(m => m.repoData, objectsEqual),180pendingRequests: Adapt.t(m => m.getPendingRequests(), Adapt.array(pendingRequestSchema)),181});182183export class ChatSessionOperationLog extends Adapt.ObjectMutationLog<IChatModel, ISerializableChatData> implements IChatDataSerializerLog {184constructor() {185super(storageSchema, 1024);186}187}188189190