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