Skip to content

Commit 3843819

Browse files
authored
Merge pull request microsoft#204349 from microsoft/joh/psychiatric-boar
Add `ChatAgentResponseStream` as a more explict alternative to `Progess<ChatAgentProgress>`
2 parents 35aa2b1 + 8d6318a commit 3843819

File tree

4 files changed

+180
-39
lines changed

4 files changed

+180
-39
lines changed

src/vs/workbench/api/common/extHostChatAgents2.ts

Lines changed: 127 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,140 @@ import { DeferredPromise, raceCancellation } from 'vs/base/common/async';
88
import { CancellationToken } from 'vs/base/common/cancellation';
99
import { toErrorMessage } from 'vs/base/common/errorMessage';
1010
import { Emitter } from 'vs/base/common/event';
11-
import { IMarkdownString } from 'vs/base/common/htmlContent';
11+
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
1212
import { StopWatch } from 'vs/base/common/stopwatch';
1313
import { URI } from 'vs/base/common/uri';
1414
import { localize } from 'vs/nls';
1515
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
1616
import { ILogService } from 'vs/platform/log/common/log';
17-
import { Progress } from 'vs/platform/progress/common/progress';
1817
import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol';
1918
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
2019
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
2120
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
2221
import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
23-
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
22+
import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
2423
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
24+
import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier';
2525
import type * as vscode from 'vscode';
2626

27+
class ChatAgentResponseStream {
28+
29+
private _stopWatch = StopWatch.create(false);
30+
private _isClosed: boolean = false;
31+
private _firstProgress: number | undefined;
32+
private _apiObject: vscode.ChatAgentExtendedResponseStream | undefined;
33+
34+
constructor(
35+
private readonly _extension: IExtensionDescription,
36+
private readonly _request: IChatAgentRequest,
37+
private readonly _proxy: MainThreadChatAgentsShape2,
38+
@ILogService private readonly _logService: ILogService,
39+
) { }
40+
41+
close() {
42+
this._isClosed = true;
43+
}
44+
45+
get timings() {
46+
return {
47+
firstProgress: this._firstProgress,
48+
totalElapsed: this._stopWatch.elapsed()
49+
};
50+
}
51+
52+
get apiObject() {
53+
54+
if (!this._apiObject) {
55+
56+
const that = this;
57+
this._stopWatch.reset();
58+
59+
function throwIfDone(source: Function | undefined) {
60+
if (that._isClosed) {
61+
const err = new Error('Response stream has been closed');
62+
Error.captureStackTrace(err, source);
63+
throw err;
64+
}
65+
}
66+
67+
const _report = (progress: Dto<IChatProgress>) => {
68+
// Measure the time to the first progress update with real markdown content
69+
if (typeof this._firstProgress === 'undefined' && 'content' in progress) {
70+
this._firstProgress = this._stopWatch.elapsed();
71+
}
72+
this._proxy.$handleProgressChunk(this._request.requestId, progress);
73+
};
74+
75+
this._apiObject = {
76+
markdown(value, meta) {
77+
throwIfDone(this.markdown);
78+
_report({
79+
kind: 'markdownContent',
80+
content: typeConvert.MarkdownString.from(value)
81+
});
82+
return this;
83+
},
84+
text(value, meta) {
85+
throwIfDone(this.text);
86+
this.markdown(new MarkdownString().appendText(value), meta);
87+
return this;
88+
},
89+
files(value, meta) {
90+
throwIfDone(this.files);
91+
_report({
92+
kind: 'treeData',
93+
treeData: value
94+
});
95+
return this;
96+
},
97+
anchor(value, meta) {
98+
throwIfDone(this.anchor);
99+
_report({
100+
kind: 'inlineReference',
101+
name: meta?.title,
102+
inlineReference: !URI.isUri(value) ? typeConvert.Location.from(<vscode.Location>value) : value
103+
});
104+
return this;
105+
},
106+
progress(value) {
107+
throwIfDone(this.progress);
108+
_report({
109+
kind: 'progressMessage',
110+
content: new MarkdownString(value)
111+
});
112+
return this;
113+
},
114+
reference(value) {
115+
throwIfDone(this.reference);
116+
_report({
117+
kind: 'reference',
118+
reference: !URI.isUri(value) ? typeConvert.Location.from(<vscode.Location>value) : value
119+
});
120+
return this;
121+
},
122+
report(progress) {
123+
throwIfDone(this.report);
124+
if ('placeholder' in progress && 'resolvedContent' in progress) {
125+
// Ignore for now, this is the deleted Task type
126+
return;
127+
}
128+
129+
const value = typeConvert.ChatResponseProgress.from(that._extension, progress);
130+
if (!value) {
131+
that._logService.error('Unknown progress type: ' + JSON.stringify(progress));
132+
return;
133+
}
134+
135+
_report(value);
136+
return this;
137+
}
138+
};
139+
}
140+
141+
return this._apiObject;
142+
}
143+
}
144+
27145
export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
28146

29147
private static _idPool = 0;
@@ -61,44 +179,17 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
61179
throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`);
62180
}
63181

64-
let done = false;
65-
function throwIfDone() {
66-
if (done) {
67-
throw new Error('Only valid while executing the command');
68-
}
69-
}
70-
71182
const commandExecution = new DeferredPromise<void>();
72183
token.onCancellationRequested(() => commandExecution.complete());
73184
this._extHostChatProvider.allowListExtensionWhile(agent.extension.identifier, commandExecution.p);
74185

75-
const stopWatch = StopWatch.create(false);
76-
let firstProgress: number | undefined;
186+
const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._logService);
77187
try {
78188
const convertedHistory = await this.prepareHistory(agent, request, context);
79189
const task = agent.invoke(
80190
typeConvert.ChatAgentRequest.to(request),
81191
{ history: convertedHistory },
82-
new Progress<vscode.ChatAgentExtendedProgress>(progress => {
83-
throwIfDone();
84-
85-
// Measure the time to the first progress update with real markdown content
86-
if (typeof firstProgress === 'undefined' && 'content' in progress) {
87-
firstProgress = stopWatch.elapsed();
88-
}
89-
90-
const convertedProgress = typeConvert.ChatResponseProgress.from(agent.extension, progress);
91-
if (!convertedProgress) {
92-
this._logService.error('Unknown progress type: ' + JSON.stringify(progress));
93-
return;
94-
}
95-
96-
if ('placeholder' in progress && 'resolvedContent' in progress) {
97-
// Ignore for now, this is the deleted Task type
98-
} else {
99-
this._proxy.$handleProgressChunk(request.requestId, convertedProgress);
100-
}
101-
}),
192+
stream.apiObject,
102193
token
103194
);
104195

@@ -112,8 +203,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
112203
}
113204
sessionResults.set(request.requestId, result);
114205

115-
const timings = { firstProgress: firstProgress, totalElapsed: stopWatch.elapsed() };
116-
return { errorDetails: result.errorDetails, timings };
206+
return { errorDetails: result.errorDetails, timings: stream.timings };
117207
} else {
118208
this._previousResultMap.delete(request.sessionId);
119209
}
@@ -126,7 +216,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
126216
return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } };
127217

128218
} finally {
129-
done = true;
219+
stream.close();
130220
commandExecution.complete();
131221
}
132222
}
@@ -514,7 +604,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
514604
} satisfies vscode.ChatAgent2<TResult>;
515605
}
516606

517-
invoke(request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, progress: Progress<vscode.ChatAgentExtendedProgress>, token: CancellationToken): vscode.ProviderResult<vscode.ChatAgentResult2> {
518-
return this._callback(request, context, progress, token);
607+
invoke(request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, response: vscode.ChatAgentExtendedResponseStream, token: CancellationToken): vscode.ProviderResult<vscode.ChatAgentResult2> {
608+
return this._callback(request, context, response, token);
519609
}
520610
}

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ export namespace Range {
123123
}
124124

125125
export namespace Location {
126+
127+
export function from(location: vscode.Location): Dto<languages.Location> {
128+
return {
129+
uri: location.uri,
130+
range: Range.from(location.range)
131+
};
132+
}
133+
126134
export function to(location: Dto<languages.Location>): vscode.Location {
127135
return new types.Location(URI.revive(location.uri), Range.to(location.range));
128136
}

src/vscode-dts/vscode.proposed.chatAgents2.d.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,57 @@ declare module 'vscode' {
274274
variables: Record<string, ChatVariableValue[]>;
275275
}
276276

277+
export interface ChatAgentResponseItemMetadata {
278+
title: string;
279+
// annotations: any[]; // future OffsetbasedAnnotation and Annotation
280+
}
281+
282+
export interface ChatAgentResponseStream {
283+
284+
// RENDERED
285+
286+
text(value: string, meta?: ChatAgentResponseItemMetadata): ChatAgentResponseStream;
287+
288+
markdown(value: string | MarkdownString, meta?: ChatAgentResponseItemMetadata): ChatAgentResponseStream;
289+
290+
files(value: ChatAgentFileTreeData, meta?: ChatAgentResponseItemMetadata): ChatAgentResponseStream;
291+
292+
anchor(value: Uri | Location, meta?: ChatAgentResponseItemMetadata): ChatAgentResponseStream;
293+
294+
// META
295+
296+
// TODO@API this influences the rendering, it inserts new lines which is likely a bug
297+
progress(value: string): ChatAgentResponseStream;
298+
299+
// TODO@API support non-file uris, like http://example.com
300+
// TODO@API support mapped edits
301+
reference(value: Uri | Location): ChatAgentResponseStream;
302+
303+
/**
304+
* @deprecated use above methods instread
305+
*/
306+
report(value: ChatAgentProgress): void;
307+
}
308+
309+
/**
310+
* @deprecated use ChatAgentResponseStream instead
311+
*/
277312
export type ChatAgentContentProgress =
278313
| ChatAgentContent
279314
| ChatAgentFileTree
280315
| ChatAgentInlineContentReference;
281316

317+
/**
318+
* @deprecated use ChatAgentResponseStream instead
319+
*/
282320
export type ChatAgentMetadataProgress =
283321
| ChatAgentUsedContext
284322
| ChatAgentContentReference
285323
| ChatAgentProgressMessage;
286324

325+
/**
326+
* @deprecated use ChatAgentResponseStream instead
327+
*/
287328
export type ChatAgentProgress = ChatAgentContentProgress | ChatAgentMetadataProgress;
288329

289330
/**
@@ -380,7 +421,7 @@ declare module 'vscode' {
380421
documents: ChatAgentDocumentContext[];
381422
}
382423

383-
export type ChatAgentHandler = (request: ChatAgentRequest, context: ChatAgentContext, progress: Progress<ChatAgentProgress>, token: CancellationToken) => ProviderResult<ChatAgentResult2>;
424+
export type ChatAgentHandler = (request: ChatAgentRequest, context: ChatAgentContext, response: ChatAgentResponseStream, token: CancellationToken) => ProviderResult<ChatAgentResult2>;
384425

385426
export namespace chat {
386427

src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ declare module 'vscode' {
4343
| ChatAgentMarkdownContent
4444
| ChatAgentDetectedAgent;
4545

46+
export type ChatAgentExtendedResponseStream = ChatAgentResponseStream & Progress<ChatAgentExtendedProgress>;
47+
4648
export interface ChatAgent2<TResult extends ChatAgentResult2> {
4749
/**
4850
* Provide a set of variables that can only be used with this agent.
@@ -64,7 +66,7 @@ declare module 'vscode' {
6466
constructor(label: string | CompletionItemLabel, values: ChatVariableValue[]);
6567
}
6668

67-
export type ChatAgentExtendedHandler = (request: ChatAgentRequest, context: ChatAgentContext, progress: Progress<ChatAgentExtendedProgress>, token: CancellationToken) => ProviderResult<ChatAgentResult2>;
69+
export type ChatAgentExtendedHandler = (request: ChatAgentRequest, context: ChatAgentContext, response: ChatAgentExtendedResponseStream, token: CancellationToken) => ProviderResult<ChatAgentResult2>;
6870

6971
export namespace chat {
7072
/**

0 commit comments

Comments
 (0)