Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/pseudoStartStopConversationCallback.ts
13399 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 * as l10n from '@vscode/l10n';
7
import { disableErrorLogging, parse as parsePartialJson } from 'best-effort-json-parser';
8
import type { ChatResponseStream, ChatVulnerability } from 'vscode';
9
import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';
10
import { IResponseDelta } from '../../../platform/networking/common/fetch';
11
import { FilterReason } from '../../../platform/networking/common/openai';
12
import { isEncryptedThinkingDelta } from '../../../platform/thinking/common/thinking';
13
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
14
import { URI } from '../../../util/vs/base/common/uri';
15
import { ChatResponseClearToPreviousToolInvocationReason } from '../../../vscodeTypes';
16
import { getContributedToolName } from '../../tools/common/toolNames';
17
import { IResponseProcessor, IResponseProcessorContext } from './intents';
18
19
disableErrorLogging();
20
21
export interface StartStopMapping {
22
readonly stop: string;
23
readonly start?: string;
24
}
25
26
/**
27
* This IConversationCallback skips over text that is between a start and stop word and processes it for output if applicable.
28
*/
29
export class PseudoStopStartResponseProcessor implements IResponseProcessor {
30
private stagedDeltasToApply: IResponseDelta[] = [];
31
private currentStartStop: StartStopMapping | undefined = undefined;
32
private nonReportedDeltas: IResponseDelta[] = [];
33
private thinkingActive: boolean = false;
34
35
private static readonly _toolStreamThrottleMs = 100;
36
private readonly _lastToolStreamUpdate = new Map<string, number>();
37
private readonly _pendingToolStreamUpdates = new Map<string, { id: string; arguments: string | undefined }>();
38
39
constructor(
40
private readonly stopStartMappings: readonly StartStopMapping[],
41
private readonly processNonReportedDelta: ((deltas: IResponseDelta[]) => string[]) | undefined,
42
private readonly options?: { subagentInvocationId?: string }
43
) { }
44
45
async processResponse(_context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: ChatResponseStream, token: CancellationToken): Promise<void> {
46
return this.doProcessResponse(inputStream, outputStream, token);
47
}
48
49
async doProcessResponse(responseStream: AsyncIterable<IResponsePart>, progress: ChatResponseStream, token: CancellationToken): Promise<void> {
50
try {
51
for await (const { delta } of responseStream) {
52
if (token.isCancellationRequested) {
53
return;
54
}
55
this.applyDelta(delta, progress);
56
}
57
} finally {
58
if (token.isCancellationRequested) {
59
this._clearPendingToolStreamUpdates();
60
} else {
61
this._flushPendingToolStreamUpdates(progress);
62
}
63
}
64
}
65
66
private _clearPendingToolStreamUpdates(): void {
67
this._pendingToolStreamUpdates.clear();
68
this._lastToolStreamUpdate.clear();
69
}
70
71
private _flushPendingToolStreamUpdates(progress: ChatResponseStream): void {
72
for (const update of this._pendingToolStreamUpdates.values()) {
73
progress.updateToolInvocation(update.id, { partialInput: tryParsePartialToolInput(update.arguments) });
74
}
75
this._clearPendingToolStreamUpdates();
76
}
77
78
protected applyDeltaToProgress(delta: IResponseDelta, progress: ChatResponseStream) {
79
if (delta.thinking) {
80
// Don't send parts that are only encrypted content
81
if (!isEncryptedThinkingDelta(delta.thinking) || delta.thinking.text) {
82
progress.thinkingProgress(delta.thinking);
83
this.thinkingActive = true;
84
}
85
} else if (this.thinkingActive) {
86
progress.thinkingProgress({ id: '', text: '', metadata: { vscodeReasoningDone: true, stopReason: delta.text ? 'text' : 'other' } });
87
this.thinkingActive = false;
88
}
89
90
reportCitations(delta, progress);
91
92
const vulnerabilities: ChatVulnerability[] | undefined = delta.codeVulnAnnotations?.map(a => ({ title: a.details.type, description: a.details.description }));
93
if (vulnerabilities?.length) {
94
progress.markdownWithVulnerabilities(delta.text ?? '', vulnerabilities);
95
} else if (delta.text) {
96
progress.markdown(delta.text);
97
}
98
99
if (delta.beginToolCalls?.length) {
100
for (const beginCall of delta.beginToolCalls) {
101
progress.beginToolInvocation(beginCall.id ?? '', getContributedToolName(beginCall.name), { subagentInvocationId: this.options?.subagentInvocationId });
102
}
103
}
104
105
if (delta.copilotToolCallStreamUpdates?.length) {
106
const now = Date.now();
107
for (const update of delta.copilotToolCallStreamUpdates) {
108
if (!update.name) {
109
continue;
110
}
111
const toolId = update.id ?? '';
112
const lastUpdate = this._lastToolStreamUpdate.get(toolId) ?? 0;
113
if (now - lastUpdate >= PseudoStopStartResponseProcessor._toolStreamThrottleMs) {
114
this._lastToolStreamUpdate.set(toolId, now);
115
this._pendingToolStreamUpdates.delete(toolId);
116
progress.updateToolInvocation(toolId, { partialInput: tryParsePartialToolInput(update.arguments) });
117
} else {
118
this._pendingToolStreamUpdates.set(toolId, { id: toolId, arguments: update.arguments });
119
}
120
}
121
}
122
}
123
124
/**
125
* Update the stagedDeltasToApply list: consume deltas up to `idx` and return them, and delete `length` after that
126
*/
127
private updateStagedDeltasUpToIndex(stopWordIdx: number, length: number): IResponseDelta[] {
128
const result: IResponseDelta[] = [];
129
for (let deltaOffset = 0; deltaOffset < stopWordIdx + length;) {
130
const delta = this.stagedDeltasToApply.shift();
131
if (delta) {
132
if (deltaOffset + delta.text.length <= stopWordIdx) {
133
// This delta is in the prefix, return it
134
result.push(delta);
135
} else if (deltaOffset < stopWordIdx || deltaOffset < stopWordIdx + length) {
136
// This delta goes over the stop word, split it
137
if (deltaOffset < stopWordIdx) {
138
const prefixDelta = { ...delta };
139
prefixDelta.text = delta.text.substring(0, stopWordIdx - deltaOffset);
140
result.push(prefixDelta);
141
}
142
143
// This is copying the annotation onto both sides of the split delta, better to be safe
144
const postfixDelta = { ...delta };
145
postfixDelta.text = delta.text.substring((stopWordIdx - deltaOffset) + length);
146
if (postfixDelta.text) {
147
this.stagedDeltasToApply.unshift(postfixDelta);
148
}
149
150
} else {
151
// This one is already over the idx, delete it
152
}
153
154
deltaOffset += delta.text.length;
155
} else {
156
break;
157
}
158
}
159
160
return result;
161
}
162
163
protected checkForKeyWords(pseudoStopWords: string[], delta: IResponseDelta, applyDeltaToProgress: (delta: IResponseDelta) => void): string | undefined {
164
const textDelta = this.stagedDeltasToApply.map(d => d.text).join('') + delta.text;
165
166
// Find out if we have a complete stop word
167
for (const pseudoStopWord of pseudoStopWords) {
168
const stopWordIndex = textDelta.indexOf(pseudoStopWord);
169
if (stopWordIndex === -1) {
170
continue;
171
}
172
173
// We have a stop word, so apply the text up to the stop word
174
this.stagedDeltasToApply.push(delta);
175
const deltasToReport = this.updateStagedDeltasUpToIndex(stopWordIndex, pseudoStopWord.length);
176
deltasToReport.forEach(item => applyDeltaToProgress(item));
177
178
return pseudoStopWord;
179
}
180
181
// We now need to find out if we have a partial stop word
182
for (const pseudoStopWord of pseudoStopWords) {
183
for (let i = pseudoStopWord.length - 1; i > 0; i--) {
184
const partialStopWord = pseudoStopWord.substring(0, i);
185
if (textDelta.endsWith(partialStopWord)) {
186
// We have a partial stop word, so we must stage the text and wait for the rest
187
this.stagedDeltasToApply = [...this.stagedDeltasToApply, delta];
188
return;
189
}
190
}
191
}
192
193
// We have no stop word or partial, so apply the text to the progress and turn
194
[...this.stagedDeltasToApply, delta].forEach(item => {
195
applyDeltaToProgress(item);
196
});
197
this.stagedDeltasToApply = [];
198
199
return;
200
}
201
202
private postReportRecordProgress(delta: IResponseDelta) {
203
this.nonReportedDeltas.push(delta);
204
}
205
206
protected applyDelta(delta: IResponseDelta, progress: ChatResponseStream): void {
207
if (delta.retryReason) {
208
this.stagedDeltasToApply = [];
209
this.currentStartStop = undefined;
210
this.nonReportedDeltas = [];
211
this.thinkingActive = false;
212
this._clearPendingToolStreamUpdates();
213
if (delta.retryReason === 'network_error' || delta.retryReason === 'server_error') {
214
progress.clearToPreviousToolInvocation(ChatResponseClearToPreviousToolInvocationReason.NoReason);
215
} else if (delta.retryReason === FilterReason.Copyright) {
216
progress.clearToPreviousToolInvocation(ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry);
217
} else {
218
progress.clearToPreviousToolInvocation(ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry);
219
}
220
return;
221
}
222
if (this.currentStartStop === undefined) {
223
const stopWord = this.checkForKeyWords(this.stopStartMappings.map(e => e.stop), delta, delta => this.applyDeltaToProgress(delta, progress));
224
if (stopWord) {
225
this.currentStartStop = this.stopStartMappings.find(e => e.stop === stopWord);
226
}
227
return;
228
} else {
229
if (!this.currentStartStop.start) {
230
return;
231
}
232
const startWord = this.checkForKeyWords([this.currentStartStop.start], delta, this.postReportRecordProgress.bind(this));
233
if (startWord) {
234
if (this.processNonReportedDelta) {
235
const postProcessed = this.processNonReportedDelta(this.nonReportedDeltas);
236
postProcessed.forEach((text) => this.applyDeltaToProgress({ text }, progress)); // processNonReportedDelta should not return anything that would have annotations
237
}
238
239
this.currentStartStop = undefined;
240
if (this.stagedDeltasToApply.length > 0) {
241
// since there's no guarantee that applyDelta will be called again, flush the stagedTextToApply by applying a blank string
242
this.applyDelta({ text: '' }, progress);
243
}
244
}
245
}
246
}
247
}
248
249
/**
250
* Note- IPCitations (snippy) are disabled in non-prod builds. See packagejson.ts, isProduction.
251
*/
252
export function reportCitations(delta: IResponseDelta, progress: ChatResponseStream): void {
253
const citations = delta.ipCitations;
254
if (citations?.length) {
255
citations.forEach(c => {
256
const licenseLabel = c.citations.license === 'NOASSERTION' ?
257
l10n.t('unknown') :
258
c.citations.license;
259
progress.codeCitation(URI.parse(c.citations.url), licenseLabel, c.citations.snippet);
260
});
261
}
262
}
263
264
/**
265
* Attempts to parse partial JSON using best-effort parsing.
266
* For streaming tool call arguments, the JSON arrives incrementally.
267
*/
268
function tryParsePartialToolInput(raw: string | undefined): unknown {
269
if (!raw) {
270
return raw;
271
}
272
273
try {
274
// Certain patterns, especially partially-generated unicode escape sequences, cause this to throw.
275
return parsePartialJson(raw);
276
} catch {
277
return undefined;
278
}
279
}
280
281