Skip to content

Commit 11f6af3

Browse files
authored
Coding Agent Improvements (#7675)
* coding agent improvements * update test
1 parent 10a2e13 commit 11f6af3

File tree

4 files changed

+90
-41
lines changed

4 files changed

+90
-41
lines changed

src/github/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function mergeQuerySchemaWithShared(sharedSchema: Schema, schema: Schema)
9999
};
100100
}
101101

102-
type RemoteAgentSuccessResult = { link: string; state: 'success'; number: number; webviewUri: Uri; llmDetails: string };
102+
type RemoteAgentSuccessResult = { link: string; state: 'success'; number: number; webviewUri: Uri; llmDetails: string; sessionId: string };
103103
type RemoteAgentErrorResult = { error: string; state: 'error' };
104104
export type RemoteAgentResult = RemoteAgentSuccessResult | RemoteAgentErrorResult;
105105

src/github/copilotApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface RemoteAgentJobResponse {
3838
html_url: string;
3939
number: number;
4040
}
41+
session_id: string;
4142
}
4243

4344
export interface ChatSessionWithPR extends vscode.ChatSessionItem {
@@ -47,6 +48,9 @@ export interface ChatSessionWithPR extends vscode.ChatSessionItem {
4748
export interface ChatSessionFromSummarizedChat extends vscode.ChatSessionItem {
4849
prompt: string;
4950
summary?: string;
51+
// Cache
52+
pullRequest?: PullRequestModel;
53+
sessionInfo?: SessionInfo;
5054
}
5155

5256
export class CopilotApi {
@@ -157,6 +161,9 @@ export class CopilotApi {
157161
if (typeof data.pull_request.number !== 'number') {
158162
throw new Error('Invalid pull_request.number in response');
159163
}
164+
if (typeof data.session_id !== 'string') {
165+
throw new Error('Invalid session_id in response');
166+
}
160167
}
161168

162169
public async getLogsFromZipUrl(logsUrl: string): Promise<string[]> {

src/github/copilotRemoteAgent.ts

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class CopilotRemoteAgentManager extends Disposable {
5959
readonly onDidChangeChatSessions = this._onDidChangeChatSessions.event;
6060

6161
private readonly gitOperationsManager: GitOperationsManager;
62-
private readonly ephemeralChatSessions: Map<string, ChatSessionFromSummarizedChat> = new Map(); // TODO: Clean these up
62+
private readonly ephemeralChatSessions: Map<string, ChatSessionFromSummarizedChat> = new Map();
6363

6464
constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) {
6565
super();
@@ -445,7 +445,7 @@ export class CopilotRemoteAgentManager extends Disposable {
445445

446446
const result = await this.invokeRemoteAgent(
447447
userPrompt,
448-
summary || userPrompt,
448+
summary,
449449
autoPushAndCommit,
450450
);
451451

@@ -510,7 +510,7 @@ export class CopilotRemoteAgentManager extends Disposable {
510510
return vscode.l10n.t('🚀 Coding agent will continue work in [#{0}]({1}). Track progress [here]({2}).', number, link, webviewUri.toString());
511511
}
512512

513-
async invokeRemoteAgent(prompt: string, problemContext: string, autoPushAndCommit = true): Promise<RemoteAgentResult> {
513+
async invokeRemoteAgent(prompt: string, problemContext?: string, autoPushAndCommit = true): Promise<RemoteAgentResult> {
514514
const capiClient = await this.copilotApi;
515515
if (!capiClient) {
516516
return { error: vscode.l10n.t('Failed to initialize Copilot API'), state: 'error' };
@@ -557,7 +557,7 @@ export class CopilotRemoteAgentManager extends Disposable {
557557
}
558558

559559
let title = prompt;
560-
const titleMatch = problemContext.match(/TITLE: \s*(.*)/i);
560+
const titleMatch = problemContext?.match(/TITLE: \s*(.*)/i);
561561
if (titleMatch && titleMatch[1]) {
562562
title = titleMatch[1].trim();
563563
}
@@ -574,15 +574,15 @@ export class CopilotRemoteAgentManager extends Disposable {
574574
event_type: 'visual_studio_code_remote_agent_tool_invoked',
575575
pull_request: {
576576
title,
577-
body_placeholder: formatBodyPlaceholder(problemContext),
577+
body_placeholder: formatBodyPlaceholder(problemContext || prompt),
578578
base_ref,
579579
body_suffix,
580580
...(hasChanges && { head_ref: ref })
581581
}
582582
};
583583

584584
try {
585-
const { pull_request } = await capiClient.postRemoteAgentJob(owner, repo, payload);
585+
const { pull_request, session_id } = await capiClient.postRemoteAgentJob(owner, repo, payload);
586586
this._onDidCreatePullRequest.fire(pull_request.number);
587587
const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: pull_request.number });
588588
const prLlmString = `The remote agent has begun work and has created a pull request. Details about the pull request are being shown to the user. If the user wants to track progress or iterate on the agent's work, they should use the pull request.`;
@@ -591,7 +591,8 @@ export class CopilotRemoteAgentManager extends Disposable {
591591
number: pull_request.number,
592592
link: pull_request.html_url,
593593
webviewUri,
594-
llmDetails: hasChanges ? `The pending changes have been pushed to branch '${ref}'. ${prLlmString}` : prLlmString
594+
llmDetails: hasChanges ? `The pending changes have been pushed to branch '${ref}'. ${prLlmString}` : prLlmString,
595+
sessionId: session_id
595596
};
596597
} catch (error) {
597598
return { error: error.message, state: 'error' };
@@ -724,8 +725,29 @@ export class CopilotRemoteAgentManager extends Disposable {
724725
return this._stateModel.getCounts();
725726
}
726727

727-
public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>; metadata?: any; }, _token: vscode.CancellationToken): Promise<ChatSessionWithPR | ChatSessionFromSummarizedChat> {
728-
const { prompt } = options;
728+
async extractHistory(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): Promise<string | undefined> {
729+
if (!history) {
730+
return;
731+
}
732+
const parts: string[] = [];
733+
for (const turn of history) {
734+
if (turn instanceof vscode.ChatRequestTurn) {
735+
parts.push(`User: ${turn.prompt}`);
736+
} else if (turn instanceof vscode.ChatResponseTurn) {
737+
const textParts = turn.response
738+
.filter(part => part instanceof vscode.ChatResponseMarkdownPart)
739+
.map(part => part.value);
740+
if (textParts.length > 0) {
741+
parts.push(`Copilot: ${textParts.join('\n')}`);
742+
}
743+
}
744+
}
745+
const fullText = parts.join('\n'); // TODO: Summarization if too long
746+
return fullText;
747+
}
748+
749+
public async provideNewChatSessionItem(options: { prompt?: string; history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>; metadata?: any; }, token: vscode.CancellationToken): Promise<ChatSessionWithPR | ChatSessionFromSummarizedChat> {
750+
const { prompt, history } = options;
729751
if (!prompt) {
730752
throw new Error(`Prompt is expected to provide a new chat session item`);
731753
}
@@ -748,29 +770,32 @@ export class CopilotRemoteAgentManager extends Disposable {
748770

749771
const result = await this.invokeRemoteAgent(
750772
prompt,
751-
prompt,
773+
(await this.extractHistory(history)),
752774
false,
753775
);
754776
if (result.state !== 'success') {
755777
Logger.error(`Failed to provide new chat session item: ${result.error}`, CopilotRemoteAgentManager.ID);
756778
throw new Error(`Failed to provide new chat session item: ${result.error}`);
757779
}
758780

759-
const { number } = result;
781+
const { number, sessionId } = result;
760782

761-
const session = await this.findPullRequestById(number, true);
762-
if (!session) {
783+
const pullRequest = await this.findPullRequestById(number, true);
784+
if (!pullRequest) {
763785
throw new Error(`Failed to find session for pull request: ${number}`);
764786
}
765-
const timeline = await session.getCopilotTimelineEvents(session);
787+
788+
await this.waitForQueuedToInProgress(sessionId, token);
789+
790+
const timeline = await pullRequest.getCopilotTimelineEvents(pullRequest);
766791
const status = copilotEventToSessionStatus(mostRecentCopilotEvent(timeline));
767-
const tooltip = await issueMarkdown(session, this.context, this.repositoriesManager);
768-
const timestampNumber = new Date(session.createdAt).getTime();
792+
const tooltip = await issueMarkdown(pullRequest, this.context, this.repositoriesManager);
793+
const timestampNumber = new Date(pullRequest.createdAt).getTime();
769794
return {
770-
id: `${session.number}`,
771-
label: session.title || `Session ${session.number}`,
795+
id: `${pullRequest.number}`,
796+
label: pullRequest.title || `Session ${pullRequest.number}`,
772797
iconPath: this.getIconForSession(status),
773-
pullRequest: session,
798+
pullRequest: pullRequest,
774799
tooltip,
775800
status,
776801
timing: {
@@ -817,10 +842,9 @@ export class CopilotRemoteAgentManager extends Disposable {
817842
return [];
818843
}
819844

820-
821845
private async newSessionFlowFromPrompt(id: string): Promise<vscode.ChatSession> {
822-
const session = this.ephemeralChatSessions.get(id);
823-
if (!session) {
846+
const chatSession = this.ephemeralChatSessions.get(id);
847+
if (!chatSession) {
824848
return this.createEmptySession();
825849
}
826850

@@ -829,13 +853,7 @@ export class CopilotRemoteAgentManager extends Disposable {
829853
return this.createEmptySession(); // TODO: Explain how to enroll repo in coding agent, etc..?
830854
}
831855
const { repo, owner } = repoInfo;
832-
833-
// Remove from ephemeral sessions
834-
this.ephemeralChatSessions.delete(id);
835-
836-
// Create a placeholder session that will invoke the remote agent when confirmed
837-
838-
const { prompt } = session;
856+
const { prompt, summary } = chatSession;
839857
const sessionRequest = new vscode.ChatRequestTurn2(
840858
prompt,
841859
undefined,
@@ -880,15 +898,16 @@ export class CopilotRemoteAgentManager extends Disposable {
880898
stream.progress('Delegating to coding agent');
881899
const result = await this.invokeRemoteAgent(
882900
prompt,
883-
prompt,
901+
summary || prompt,
884902
false,
885903
);
904+
this.ephemeralChatSessions.delete(id); // TODO: Better state management
886905
if (result.state !== 'success') {
887906
stream.warning(`Could not create coding agent session: ${result.error}`);
888907
return {};
889908
}
890-
891909
const pullRequest = await this.findPullRequestById(result.number, true);
910+
chatSession.pullRequest = pullRequest; // Cache for later
892911
if (!pullRequest) {
893912
stream.warning(`Could not find coding agent session.`);
894913
return {};
@@ -898,16 +917,9 @@ export class CopilotRemoteAgentManager extends Disposable {
898917
stream.warning(vscode.l10n.t('Could not initialize Copilot API.'));
899918
return {};
900919
}
901-
// Poll for the new session
902-
const sessions = await capi.getAllSessions(pullRequest.id);
903-
const newSession = sessions.find(s => s.state === 'in_progress' || s.state === 'queued');
904-
if (!newSession) {
905-
stream.warning(vscode.l10n.t('Could not find coding agent session in progress.'));
906-
return {};
907-
}
908920
stream.markdown(vscode.l10n.t('Coding agent is now working on your request...'));
909921
stream.markdown('\n\n');
910-
await this.streamSessionLogs(stream, pullRequest, newSession.id, token);
922+
await this.streamSessionLogs(stream, pullRequest, result.sessionId, token);
911923
return {};
912924
default:
913925
Logger.error(`Unknown confirmation state: ${state}`, CopilotRemoteAgentManager.ID);
@@ -1380,6 +1392,35 @@ export class CopilotRemoteAgentManager extends Disposable {
13801392
};
13811393
}
13821394

1395+
private async waitForQueuedToInProgress(
1396+
sessionId: string,
1397+
token: vscode.CancellationToken
1398+
): Promise<SessionInfo | undefined> {
1399+
const capi = await this.copilotApi;
1400+
if (!capi) {
1401+
return undefined;
1402+
}
1403+
1404+
const maxWaitTime = 2 * 60 * 1_000; // 2 minutes
1405+
const pollInterval = 3_000; // 3 seconds
1406+
const startTime = Date.now();
1407+
1408+
const sessionInfo = await capi.getSessionInfo(sessionId);
1409+
if (!sessionInfo || sessionInfo.state !== 'queued') {
1410+
return;
1411+
}
1412+
1413+
Logger.appendLine(`Session ${sessionInfo.id} is queued, waiting to start...`, CopilotRemoteAgentManager.ID);
1414+
while (Date.now() - startTime < maxWaitTime && !token.isCancellationRequested) {
1415+
const sessionInfo = await capi.getSessionInfo(sessionId);
1416+
if (sessionInfo?.state === 'in_progress') {
1417+
Logger.appendLine(`Session ${sessionInfo.id} now in progress.`, CopilotRemoteAgentManager.ID);
1418+
return sessionInfo;
1419+
}
1420+
await new Promise(resolve => setTimeout(resolve, pollInterval));
1421+
}
1422+
}
1423+
13831424
private async waitForNewSession(
13841425
pullRequest: PullRequestModel,
13851426
stream: vscode.ChatResponseStream,

src/test/lm/tools/copilotRemoteAgentTool.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ describe('CopilotRemoteAgentTool', function () {
285285
number: 789,
286286
link: 'https://github.com/test/test-repo/pull/789',
287287
webviewUri: vscode.Uri.parse('https://example.com'),
288-
llmDetails: 'Agent created PR successfully'
288+
llmDetails: 'Agent created PR successfully',
289+
sessionId: '123-456'
289290
};
290291

291292
mockManager.invokeRemoteAgent.resolves(successResult);

0 commit comments

Comments
 (0)