Skip to content

Commit 6c22451

Browse files
committed
Add ChatAgentResponseStream as a more explict alternative to Progess<ChatAgentProgress>
1 parent 1ade6d2 commit 6c22451

File tree

4 files changed

+176
-39
lines changed

4 files changed

+176
-39
lines changed

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

Lines changed: 130 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,143 @@ 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) {
77+
throwIfDone(this.markdown);
78+
_report({
79+
kind: 'markdownContent',
80+
content: typeConvert.MarkdownString.from(value)
81+
});
82+
return this;
83+
},
84+
text(value) {
85+
throwIfDone(this.text);
86+
this.markdown(new MarkdownString().appendText(value));
87+
return this;
88+
},
89+
files(value) {
90+
throwIfDone(this.files);
91+
_report({
92+
kind: 'treeData',
93+
treeData: value
94+
});
95+
return this;
96+
},
97+
anchor(value) {
98+
throwIfDone(this.anchor);
99+
_report({
100+
kind: 'inlineReference',
101+
inlineReference: !URI.isUri(value) ? typeConvert.Location.from(<vscode.Location>value) : value
102+
});
103+
return this;
104+
},
105+
progress(value) {
106+
throwIfDone(this.progress);
107+
_report({
108+
kind: 'progressMessage',
109+
content: new MarkdownString(value)
110+
});
111+
return this;
112+
},
113+
reference(value) {
114+
throwIfDone(this.reference);
115+
_report({
116+
kind: 'reference',
117+
reference: !URI.isUri(value) ? typeConvert.Location.from(<vscode.Location>value) : value
118+
});
119+
return this;
120+
},
121+
// annotation(value) {
122+
// _report(value);
123+
// return this;
124+
// },
125+
report(progress) {
126+
throwIfDone(this.report);
127+
if ('placeholder' in progress && 'resolvedContent' in progress) {
128+
// Ignore for now, this is the deleted Task type
129+
return;
130+
}
131+
132+
const value = typeConvert.ChatResponseProgress.from(that._extension, progress);
133+
if (!value) {
134+
that._logService.error('Unknown progress type: ' + JSON.stringify(progress));
135+
return;
136+
}
137+
138+
_report(value);
139+
return this;
140+
}
141+
};
142+
}
143+
144+
return this._apiObject;
145+
}
146+
}
147+
27148
export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
28149

29150
private static _idPool = 0;
@@ -61,44 +182,17 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
61182
throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`);
62183
}
63184

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

75-
const stopWatch = StopWatch.create(false);
76-
let firstProgress: number | undefined;
189+
const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._logService);
77190
try {
78191
const convertedHistory = await this.prepareHistory(agent, request, context);
79192
const task = agent.invoke(
80193
typeConvert.ChatAgentRequest.to(request),
81194
{ 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-
}),
195+
stream.apiObject,
102196
token
103197
);
104198

@@ -112,8 +206,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
112206
}
113207
sessionResults.set(request.requestId, result);
114208

115-
const timings = { firstProgress: firstProgress, totalElapsed: stopWatch.elapsed() };
116-
return { errorDetails: result.errorDetails, timings };
209+
return { errorDetails: result.errorDetails, timings: stream.timings };
117210
} else {
118211
this._previousResultMap.delete(request.sessionId);
119212
}
@@ -126,7 +219,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
126219
return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } };
127220

128221
} finally {
129-
done = true;
222+
stream.close();
130223
commandExecution.complete();
131224
}
132225
}
@@ -514,7 +607,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
514607
} satisfies vscode.ChatAgent2<TResult>;
515608
}
516609

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);
610+
invoke(request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, response: vscode.ChatAgentExtendedResponseStream, token: CancellationToken): vscode.ProviderResult<vscode.ChatAgentResult2> {
611+
return this._callback(request, context, response, token);
519612
}
520613
}

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: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,50 @@ declare module 'vscode' {
274274
variables: Record<string, ChatVariableValue[]>;
275275
}
276276

277+
export interface ChatAgentResponseStream {
278+
279+
// RENDERED
280+
markdown(value: string | MarkdownString): ChatAgentResponseStream;
281+
text(value: string): ChatAgentResponseStream;
282+
files(value: ChatAgentFileTreeData): ChatAgentResponseStream;
283+
// TODO@jrieken is this sugar for markdown syntax? should we have more like codeblock, bulletlist etc?
284+
anchor(value: Uri | Location, attributes?: { title?: string }): ChatAgentResponseStream;
285+
286+
// META
287+
// TODO@API this influences the rendering, it inserts new lines which is likely a bug
288+
progress(value: string): ChatAgentResponseStream;
289+
290+
// TODO@API support non-file uris, like http://example.com
291+
reference(value: Uri | Location): ChatAgentResponseStream;
292+
293+
// TODO@API define support annotations
294+
// annotation(value: string | MarkdownString | ChatXYZAnnotation, references?: string): ChatAgentResponseStream;
295+
296+
/**
297+
* @deprecated use above methods instread
298+
*/
299+
report(value: ChatAgentProgress): void;
300+
}
301+
302+
/**
303+
* @deprecated use ChatAgentResponseStream instead
304+
*/
277305
export type ChatAgentContentProgress =
278306
| ChatAgentContent
279307
| ChatAgentFileTree
280308
| ChatAgentInlineContentReference;
281309

310+
/**
311+
* @deprecated use ChatAgentResponseStream instead
312+
*/
282313
export type ChatAgentMetadataProgress =
283314
| ChatAgentUsedContext
284315
| ChatAgentContentReference
285316
| ChatAgentProgressMessage;
286317

318+
/**
319+
* @deprecated use ChatAgentResponseStream instead
320+
*/
287321
export type ChatAgentProgress = ChatAgentContentProgress | ChatAgentMetadataProgress;
288322

289323
/**
@@ -380,7 +414,7 @@ declare module 'vscode' {
380414
documents: ChatAgentDocumentContext[];
381415
}
382416

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

385419
export namespace chat {
386420

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)