1
>
/*---------------------------------------------------------------------------------------------
chatModel.ts
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 { asArray } from '../../../../../base/common/arrays.js';
7
>
import { softAssertNever } from '../../../../../base/common/assert.js';
8
>
import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffer.js';
9
>
import { IStringDictionary } from '../../../../../base/common/collections.js';
10
>
import { BugIndicatingError } from '../../../../../base/common/errors.js';
11
>
import { Emitter, Event } from '../../../../../base/common/event.js';
12
>
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js';
13
>
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
14
>
import { ResourceMap } from '../../../../../base/common/map.js';
15
>
import { revive } from '../../../../../base/common/marshalling.js';
16
>
import { Schemas } from '../../../../../base/common/network.js';
17
>
import { equals } from '../../../../../base/common/objects.js';
18
>
import { IObservable, autorun, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts, registerAutorunSelfDisposable } from '../../../../../base/common/observable.js';
19
>
import { basename, isEqual } from '../../../../../base/common/resources.js';
20
>
import { hasKey, WithDefinedProps } from '../../../../../base/common/types.js';
21
>
import { URI, UriDto } from '../../../../../base/common/uri.js';
22
>
import { generateUuid } from '../../../../../base/common/uuid.js';
23
>
import { IRange } from '../../../../../editor/common/core/range.js';
24
>
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
25
>
import { ISelection } from '../../../../../editor/common/core/selection.js';
26
>
import { TextEdit } from '../../../../../editor/common/languages.js';
27
>
import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';
28
>
import { localize } from '../../../../../nls.js';
29
>
import { canLog, ILogService, LogLevel } from '../../../../../platform/log/common/log.js';
30
>
import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
31
>
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js';
32
>
import { migrateLegacyTerminalToolSpecificData } from '../chat.js';
33
>
import { ChatPerfMark, markChat } from '../chatPerf.js';
34
>
import { ChatAgentVoteDirection, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalEdit, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatInfoMessage, IChatLocationData, IChatMarkdownContent, IChatMcpAuthenticationRequired, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMcpServersStartingSlow, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPlanReview, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionTiming, IChatSystemNotificationPart, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsagePromptTokenDetail, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, ToolConfirmKind, isIUsedContext } from '../chatService/chatService.js';
35
>
import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js';
36
>
import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js';
37
>
import { ChatPlanReviewData } from './chatProgressTypes/chatPlanReviewData.js';
38
>
import { ChatQuestionCarouselData } from './chatProgressTypes/chatQuestionCarouselData.js';
39
>
import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js';
40
>
import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js';
41
>
import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js';
42
>
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js';
43
>
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js';
44
>
import { chatSessionResourceToId, LocalChatSessionUri } from './chatUri.js';
45
>
import { ObjectMutationLog } from './objectMutationLog.js';
46
>
47
>
48
>
/**
49
>
* Represents a queued chat request waiting to be processed.
50
>
*/
51
>
export interface IChatPendingRequest {
52
>
readonly request: IChatRequestModel;
53
>
readonly kind: ChatRequestQueueKind;
54
>
/**
55
>
* The options that were passed to sendRequest when this request was queued.
56
>
* userSelectedTools is snapshotted to a static observable at queue time.
57
>
*/
58
>
readonly sendOptions: IChatSendRequestOptions;
59
>
}
60
>
61
>
/**
62
>
* Serializable version of IChatSendRequestOptions for pending requests.
63
>
* Excludes observables and non-serializable fields.
64
>
*/
65
>
export interface ISerializableSendOptions {
66
>
modeInfo?: IChatRequestModeInfo;
67
>
userSelectedModelId?: string;
68
>
userSelectedModelConfiguration?: IStringDictionary<unknown>;
69
>
/** Static snapshot of user-selected tools (not an observable) */
70
>
userSelectedTools?: UserSelectedTools;
71
>
location?: ChatAgentLocation;
72
>
locationData?: IChatLocationData;
73
>
attempt?: number;
74
>
noCommandDetection?: boolean;
75
>
agentId?: string;
76
>
agentIdSilent?: string;
77
>
slashCommand?: string;
78
>
confirmation?: string;
79
>
isSystemInitiated?: boolean;
80
>
systemInitiatedLabel?: string;
81
>
terminalExecutionId?: string;
82
>
}
83
>
84
>
/**
85
>
* Serializable representation of a pending chat request.
86
>
*/
87
>
export interface ISerializablePendingRequestData {
88
>
id: string;
89
>
request: ISerializableChatRequestData;
90
>
kind: ChatRequestQueueKind;
91
>
sendOptions: ISerializableSendOptions;
92
>
}
93
>
94
>
export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {
95
>
png: 'image/png',
96
>
jpg: 'image/jpeg',
97
>
jpeg: 'image/jpeg',
98
>
gif: 'image/gif',
99
>
webp: 'image/webp',
100
>
};
101
>
102
>
export function getAttachableImageExtension(mimeType: string): string | undefined {
103
return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0];
104
}
106
>
export interface IChatRequestVariableData {
107
>
variables: readonly IChatRequestVariableEntry[];
108
>
}
109
>
110
>
export namespace IChatRequestVariableData {
111
>
export function toExport(data: IChatRequestVariableData): IChatRequestVariableData {
112
return { variables: data.variables.map(IChatRequestVariableEntry.toExport) };
113
}
115
>
116
>
export interface IChatRequestModel {
117
>
readonly id: string;
118
>
readonly timestamp: number;
119
>
readonly requestTimestamp: number | undefined;
120
>
readonly version: number;
121
>
readonly modeInfo?: IChatRequestModeInfo;
122
>
readonly session: IChatModel;
123
>
readonly message: IParsedChatRequest;
124
>
readonly attempt: number;
125
>
readonly variableData: IChatRequestVariableData;
126
>
readonly confirmation?: string;
127
>
readonly locationData?: IChatLocationData;
128
>
readonly attachedContext?: IChatRequestVariableEntry[];
129
>
readonly isCompleteAddedRequest: boolean;
130
>
readonly isTerminalCommand: boolean;
131
>
readonly response?: IChatResponseModel;
132
>
readonly editedFileEvents?: IChatAgentEditedFileEvent[];
133
>
shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
134
>
readonly shouldBeBlocked: IObservable<boolean>;
135
>
setShouldBeBlocked(value: boolean): void;
136
>
readonly modelId?: string;
137
>
readonly userSelectedTools?: UserSelectedTools;
138
>
readonly isSystemInitiated?: boolean;
139
>
readonly systemInitiatedLabel?: string;
140
>
readonly terminalExecutionId?: string;
141
>
}
142
>
143
>
export interface ICodeBlockInfo {
144
>
readonly suggestionId: EditSuggestionId;
145
>
}
146
>
147
>
export interface IChatTextEditGroupState {
148
>
sha1: string;
149
>
applied: number;
150
>
}
151
>
152
>
export interface IChatTextEditGroup {
153
>
uri: URI;
154
>
edits: TextEdit[][];
155
>
state?: IChatTextEditGroupState;
156
>
kind: 'textEditGroup';
157
>
done: boolean | undefined;
158
>
isExternalEdit?: boolean;
159
>
}
160
>
161
>
export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation {
162
const candidate = value as ICellTextEditOperation;
163
return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri);
164
}
166
>
export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] {
167
return value.some(isCellTextEditOperation);
168
}
170
>
export interface ICellTextEditOperation {
171
>
edit: TextEdit;
172
>
uri: URI;
173
>
}
174
>
175
>
export interface IChatNotebookEditGroup {
176
>
uri: URI;
177
>
edits: (ICellTextEditOperation[] | ICellEditOperation[])[];
178
>
state?: IChatTextEditGroupState;
179
>
kind: 'notebookEditGroup';
180
>
done: boolean | undefined;
181
>
isExternalEdit?: boolean;
182
>
}
183
>
184
>
/**
185
>
* Progress kinds that are included in the history of a response.
186
>
* Excludes "internal" types that are included in history.
187
>
*/
188
>
export type IChatProgressHistoryResponseContent =
189
>
| IChatMarkdownContent
190
>
| IChatAgentMarkdownContentWithVulnerability
191
>
| IChatResponseCodeblockUriPart
192
>
| IChatTreeData
193
>
| IChatMultiDiffDataSerialized
194
>
| IChatContentInlineReference
195
>
| IChatProgressMessage
196
>
| IChatSystemNotificationPart
197
>
| IChatCommandButton
198
>
| IChatWarningMessage
199
>
| IChatInfoMessage
200
>
| IChatTask
201
>
| IChatTaskSerialized
202
>
| IChatTextEditGroup
203
>
| IChatNotebookEditGroup
204
>
| IChatConfirmation
205
>
| IChatQuestionCarousel
206
>
| IChatPlanReview
207
>
| IChatExtensionsContent
208
>
| IChatThinkingPart
209
>
| IChatHookPart
210
>
| IChatPullRequestContent
211
>
| IChatWorkspaceEdit
212
>
| IChatExternalEdit
213
>
| IChatAutoModeResolutionPart;
214
>
215
>
/**
216
>
* "Normal" progress kinds that are rendered as parts of the stream of content.
217
>
*/
218
>
export type IChatProgressResponseContent =
219
>
| IChatProgressHistoryResponseContent
220
>
| IChatToolInvocation
221
>
| IChatToolInvocationSerialized
222
>
| IChatMultiDiffData
223
>
| IChatUndoStop
224
>
| IChatElicitationRequest
225
>
| IChatElicitationRequestSerialized
226
>
| IChatClearToPreviousToolInvocation
227
>
| IChatMcpServersStarting
228
>
| IChatMcpServersStartingSerialized
229
>
| IChatMcpAuthenticationRequired
230
>
| IChatMcpServersStartingSlow
231
>
| IChatDisabledClaudeHooksPart;
232
>
233
>
export type IChatProgressResponseContentSerialized = Exclude<IChatProgressResponseContent,
234
>
| IChatToolInvocation
235
>
| IChatElicitationRequest
236
>
| IChatTask
237
>
| IChatMultiDiffData
238
>
| IChatMcpServersStarting
239
>
| IChatMcpAuthenticationRequired
240
>
| IChatMcpServersStartingSlow
241
>
| IChatDisabledClaudeHooksPart
242
>
>;
243
>
244
>
const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']);
245
function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent {
246
return !nonHistoryKinds.has(content.kind);
247
}
249
>
export function toChatHistoryContent(content: ReadonlyArray<IChatProgressResponseContent>): IChatProgressHistoryResponseContent[] {
250
return content.filter(isChatProgressHistoryResponseContent);
251
}
253
>
export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>;
254
>
255
>
export interface IResponse {
256
>
readonly value: ReadonlyArray<IChatProgressResponseContent>;
257
>
getMarkdown(): string;
258
>
getFinalResponse(): string;
259
>
toString(): string;
260
>
}
261
>
262
>
export interface IChatResponseModel {
263
>
readonly onDidChange: Event<ChatResponseModelChangeReason>;
264
>
readonly id: string;
265
>
readonly requestId: string;
266
>
readonly request: IChatRequestModel | undefined;
267
>
readonly username: string;
268
>
readonly session: IChatModel;
269
>
readonly agent?: IChatAgentData;
270
>
readonly usedContext: IChatUsedContext | undefined;
271
>
readonly contentReferences: ReadonlyArray<IChatContentReference>;
272
>
readonly codeCitations: ReadonlyArray<IChatCodeCitation>;
273
>
readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
274
>
readonly slashCommand?: IChatAgentCommand;
275
>
readonly agentOrSlashCommandDetected: boolean;
276
>
/** View of the response shown to the user, may have parts omitted from undo stops. */
277
>
readonly response: IResponse;
278
>
/** Entire response from the model. */
279
>
readonly entireResponse: IResponse;
280
>
/** Milliseconds timestamp when this chat response was created. */
281
>
readonly timestamp: number;
282
>
/** Milliseconds timestamp when this chat response was completed or cancelled. */
283
>
readonly completedAt?: number;
284
>
/** Known completion timestamp for display. Undefined for legacy responses whose completion time was synthesized during restore. */
285
>
readonly completionTimestamp?: number;
286
>
/** The state of this response */
287
>
readonly state: ResponseModelState;
288
>
/** @internal */
289
>
readonly stateT: ResponseModelStateT;
290
>
/**
291
>
* Adjusted millisecond timestamp that excludes the duration during which
292
>
* the model was pending user confirmation. `Date.now() - confirmationAdjustedTimestamp`
293
>
* will return the amount of time the response was busy generating content.
294
>
* This is updated only when `isPendingConfirmation` changes state.
295
>
*/
296
>
readonly confirmationAdjustedTimestamp: IObservable<number>;
297
>
readonly isComplete: boolean;
298
>
readonly isCanceled: boolean;
299
>
readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;
300
>
readonly isInProgress: IObservable<boolean>;
301
>
/**
302
>
* True whenever this response has not reached a terminal state yet.
303
>
* Unlike {@link isInProgress}, this remains true during tool confirmations,
304
>
* elicitations, and any other intermediate state.
305
>
*/
306
>
readonly isIncomplete: IObservable<boolean>;
307
>
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
308
>
readonly shouldBeBlocked: IObservable<boolean>;
309
>
readonly isCompleteAddedRequest: boolean;
310
>
/** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */
311
>
readonly isStale: boolean;
312
>
readonly vote: ChatAgentVoteDirection | undefined;
313
>
readonly followups?: IChatFollowup[] | undefined;
314
>
readonly result?: IChatAgentResult;
315
>
readonly usage?: IChatUsage;
316
>
readonly usageObs: IObservable<IChatUsage | undefined>;
317
>
readonly completionTokenCount: number | undefined;
318
>
readonly completionTokenCountObs: IObservable<number | undefined>;
319
>
/** Elapsed generation time in ms (excluding confirmation waits). Set on completion and serialized. */
320
>
readonly elapsedMs: number | undefined;
321
>
readonly codeBlockInfos: ICodeBlockInfo[] | undefined;
322
>
323
>
initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void;
324
>
addUndoStop(undoStop: IChatUndoStop): void;
325
>
setVote(vote: ChatAgentVoteDirection): void;
326
>
setUsage(usage: IChatUsage): void;
327
>
setElapsedMs(elapsedMs: number): void;
328
>
setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean;
329
>
resolveInlineReference(resolveId: string, resolvedReference: IChatContentInlineReference): boolean;
330
>
updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask | IChatExternalToolInvocationUpdate, quiet?: boolean): void;
331
>
/**
332
>
* Adopts any partially-undo {@link response} as the {@link entireResponse}.
333
>
* Only valid when {@link isComplete}. This is needed because otherwise an
334
>
* undone and then diverged state would start showing old data because the
335
>
* undo stops would no longer exist in the model.
336
>
*/
337
>
finalizeUndoState(): void;
338
>
}
339
>
340
>
export type ChatResponseModelChangeReason =
341
>
| { reason: 'other' }
342
>
| { reason: 'completedRequest' }
343
>
| { reason: 'undoStop'; id: string };
344
>
345
>
export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' };
346
>
347
>
export interface IChatRequestModeInfo {
348
>
kind: ChatModeKind | undefined; // is undefined in case of telemetryModeId === 'applyCodeBlock'
349
>
isBuiltin: boolean;
350
>
modeInstructions: IChatRequestModeInstructions | undefined;
351
>
telemetryModeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined;
352
>
telemetryModeName?: string;
353
>
applyCodeBlockSuggestionId: EditSuggestionId | undefined;
354
>
permissionLevel?: ChatPermissionLevel;
355
>
}
356
>
357
>
export interface IChatRequestModeInstructions {
358
>
readonly uri?: URI;
359
>
readonly name: string;
360
>
readonly content: string;
361
>
readonly toolReferences: readonly ChatRequestToolReferenceEntry[];
362
>
readonly allowedSubagents?: readonly string[];
363
>
readonly metadata?: Record<string, boolean | string | number>;
364
>
readonly isBuiltin?: boolean;
365
>
}
366
>
367
>
export interface IChatRequestModelParameters {
368
>
session: ChatModel;
369
>
message: IParsedChatRequest;
370
>
variableData: IChatRequestVariableData;
371
>
timestamp?: number;
372
>
fallbackTimestamp?: number;
373
>
attempt?: number;
374
>
modeInfo?: IChatRequestModeInfo;
375
>
confirmation?: string;
376
>
locationData?: IChatLocationData;
377
>
attachedContext?: IChatRequestVariableEntry[];
378
>
isCompleteAddedRequest?: boolean;
379
>
modelId?: string;
380
>
restoredId?: string;
381
>
editedFileEvents?: IChatAgentEditedFileEvent[];
382
>
userSelectedTools?: UserSelectedTools;
383
>
isSystemInitiated?: boolean;
384
>
systemInitiatedLabel?: string;
385
>
terminalExecutionId?: string;
386
>
/** Whether this request runs as a terminal command (agent host `!` prefix). */
387
>
isTerminalCommand?: boolean;
388
>
}
389
>
390
>
export class ChatRequestModel implements IChatRequestModel {
391
>
public readonly id: string;
392
>
public response: ChatResponseModel | undefined;
393
>
public shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
394
>
public readonly timestamp: number;
395
>
public readonly requestTimestamp: number | undefined;
396
>
public readonly message: IParsedChatRequest;
397
>
public readonly isCompleteAddedRequest: boolean;
398
>
public readonly modelId?: string;
399
>
public readonly modeInfo?: IChatRequestModeInfo;
400
>
public readonly userSelectedTools?: UserSelectedTools;
401
>
public readonly isSystemInitiated?: boolean;
402
>
public readonly systemInitiatedLabel?: string;
403
>
public readonly terminalExecutionId?: string;
404
>
public readonly isTerminalCommand: boolean;
405
>
406
>
private readonly _shouldBeBlocked = observableValue<boolean>(this, false);
407
>
public get shouldBeBlocked(): IObservable<boolean> {
408
>
return this._shouldBeBlocked;
409
>
}
410
>
411
>
public setShouldBeBlocked(value: boolean): void {
412
this._shouldBeBlocked.set(value, undefined);
413
}
415
>
private _session: ChatModel;
416
>
private readonly _attempt: number;
417
>
private _variableData: IChatRequestVariableData;
418
>
private readonly _confirmation?: string;
419
>
private readonly _locationData?: IChatLocationData;
420
>
private readonly _attachedContext?: IChatRequestVariableEntry[];
421
>
private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];
422
>
423
>
public get session(): ChatModel {
424
return this._session;
425
}
427
>
public get attempt(): number {
428
return this._attempt;
429
}
431
>
public get variableData(): IChatRequestVariableData {
432
return this._variableData;
433
}
435
>
public set variableData(v: IChatRequestVariableData) {
436
this._version++;
437
this._variableData = v;
438
}
440
>
public get confirmation(): string | undefined {
441
return this._confirmation;
442
}
444
>
public get locationData(): IChatLocationData | undefined {
445
return this._locationData;
446
}
448
>
public get attachedContext(): IChatRequestVariableEntry[] | undefined {
449
return this._attachedContext;
450
}
452
>
public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {
453
return this._editedFileEvents;
454
}
456
>
private _version = 0;
457
>
public get version(): number {
458
return this._version;
459
}
461
>
constructor(params: IChatRequestModelParameters) {
462
this._session = params.session;
463
this.message = params.message;