Skip to content

Commit 40ffcba

Browse files
authored
Adopt CAPI jobs API v1 (#7943)
1 parent 0739370 commit 40ffcba

2 files changed

Lines changed: 100 additions & 22 deletions

File tree

src/github/copilotApi.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const LEARN_MORE_URL = 'https://aka.ms/coding-agent-docs';
2121
const PREMIUM_REQUESTS_URL = 'https://docs.github.com/en/copilot/concepts/copilot-billing/understanding-and-managing-requests-in-copilot#what-are-premium-requests';
2222
// https://github.com/github/sweagentd/blob/59e7d9210ca3ebba029918387e525eea73cb1f4a/internal/problemstatement/problemstatement.go#L36-L53
2323
export const MAX_PROBLEM_STATEMENT_LENGTH = 30_000 - 50; // 50 character buffer
24+
// https://github.com/github/sweagentd/blob/0ad8f81a9c64754cb8a83d10777de4638bba1a6e/docs/adr/0001-create-job-api.md#post-jobsownerrepo---create-job-task
25+
const JOBS_API_VERSION = 'v1';
26+
2427
export interface RemoteAgentJobPayload {
2528
problem_statement: string;
2629
event_type: string;
@@ -35,11 +38,14 @@ export interface RemoteAgentJobPayload {
3538
}
3639

3740
export interface RemoteAgentJobResponse {
38-
pull_request: {
39-
html_url: string;
40-
number: number;
41-
}
41+
job_id: string;
4242
session_id: string;
43+
actor: {
44+
id: number;
45+
login: string;
46+
};
47+
created_at: string;
48+
updated_at: string;
4349
}
4450

4551
export interface ChatSessionWithPR extends vscode.ChatSessionItem {
@@ -81,7 +87,7 @@ export class CopilotApi {
8187
isTruncated: boolean,
8288
): Promise<RemoteAgentJobResponse> {
8389
const repoSlug = `${owner}/${name}`;
84-
const apiUrl = `/agents/swe/v0/jobs/${repoSlug}`;
90+
const apiUrl = `/agents/swe/${JOBS_API_VERSION}/jobs/${repoSlug}`;
8591
let status: number | undefined;
8692

8793
const problemStatementLength = payload.problem_statement.length.toString();
@@ -169,18 +175,27 @@ export class CopilotApi {
169175
if (!data || typeof data !== 'object') {
170176
throw new Error('Invalid response from coding agent');
171177
}
172-
if (!data.pull_request || typeof data.pull_request !== 'object') {
173-
throw new Error('Invalid pull_request in response');
174-
}
175-
if (typeof data.pull_request.html_url !== 'string') {
176-
throw new Error('Invalid pull_request.html_url in response');
177-
}
178-
if (typeof data.pull_request.number !== 'number') {
179-
throw new Error('Invalid pull_request.number in response');
178+
if (typeof data.job_id !== 'string') {
179+
throw new Error('Invalid job_id in response');
180180
}
181181
if (typeof data.session_id !== 'string') {
182182
throw new Error('Invalid session_id in response');
183183
}
184+
if (!data.actor || typeof data.actor !== 'object') {
185+
throw new Error('Invalid actor in response');
186+
}
187+
if (typeof data.actor.id !== 'number') {
188+
throw new Error('Invalid actor.id in response');
189+
}
190+
if (typeof data.actor.login !== 'string') {
191+
throw new Error('Invalid actor.login in response');
192+
}
193+
if (typeof data.created_at !== 'string') {
194+
throw new Error('Invalid created_at in response');
195+
}
196+
if (typeof data.updated_at !== 'string') {
197+
throw new Error('Invalid updated_at in response');
198+
}
184199
}
185200

186201
public async getLogsFromZipUrl(logsUrl: string): Promise<string[]> {
@@ -270,9 +285,32 @@ export class CopilotApi {
270285
return await logsResponse.text();
271286
}
272287

288+
public async getJobByJobId(owner: string, repo: string, jobId: string): Promise<JobInfo | undefined> {
289+
try {
290+
const response = await this.makeApiCall(`/agents/swe/v1/jobs/${owner}/${repo}/${jobId}`, {
291+
method: 'GET',
292+
headers: {
293+
'Authorization': `Bearer ${this.token}`,
294+
'Content-Type': 'application/json',
295+
'Accept': 'application/json',
296+
'User-Agent': this.userAgent,
297+
}
298+
});
299+
if (!response.ok) {
300+
Logger.warn(`Failed to fetch job info for job ${jobId}: ${response.statusText}`, CopilotApi.ID);
301+
return;
302+
}
303+
const data = await response.json() as JobInfo;
304+
return data;
305+
} catch (error) {
306+
Logger.warn(`Error fetching job info for job ${jobId}: ${error}`, CopilotApi.ID);
307+
return;
308+
}
309+
}
310+
273311
public async getJobBySessionId(owner: string, repo: string, sessionId: string): Promise<JobInfo | undefined> {
274312
try {
275-
const response = await this.makeApiCall(`/agents/swe/v0/jobs/${owner}/${repo}/session/${sessionId}`, {
313+
const response = await this.makeApiCall(`/agents/swe/${JOBS_API_VERSION}/jobs/${owner}/${repo}/session/${sessionId}`, {
276314
method: 'GET',
277315
headers: {
278316
'Authorization': `Bearer ${this.token}`,

src/github/copilotRemoteAgent.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH } from '../common/setti
1919
import { ITelemetry } from '../common/telemetry';
2020
import { toOpenPullRequestWebviewUri } from '../common/uri';
2121
import { copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common';
22-
import { ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi';
22+
import { ChatSessionWithPR, CopilotApi, getCopilotApi, JobInfo, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi';
2323
import { CodingAgentPRAndStatus, CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
2424
import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder';
2525
import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager';
@@ -784,19 +784,33 @@ export class CopilotRemoteAgentManager extends Disposable {
784784
};
785785

786786
try {
787-
const { pull_request, session_id } = await capiClient.postRemoteAgentJob(owner, repo, payload, isTruncated);
788-
this._onDidCreatePullRequest.fire(pull_request.number);
789-
const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: pull_request.number });
787+
const response = await capiClient.postRemoteAgentJob(owner, repo, payload, isTruncated);
788+
789+
// For v1 API, we need to fetch the job details to get the PR info
790+
// Since the PR might not be created immediately, we need to poll for it
791+
const jobInfo = await this.waitForJobWithPullRequest(capiClient, owner, repo, response.job_id, token);
792+
if (!jobInfo || !jobInfo.pull_request) {
793+
return { error: vscode.l10n.t('Failed to retrieve pull request information from job'), state: 'error' };
794+
}
795+
796+
const { number } = jobInfo.pull_request;
797+
this._onDidCreatePullRequest.fire(number);
798+
799+
// Find the actual PR to get the HTML URL
800+
const pullRequest = await this.findPullRequestById(number, true);
801+
const htmlUrl = pullRequest?.html_url || `https://github.com/${owner}/${repo}/pull/${number}`;
802+
803+
const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: number });
790804
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.`;
791805

792-
await this.waitForQueuedToInProgress(session_id, token);
806+
await this.waitForQueuedToInProgress(response.session_id, token);
793807
return {
794808
state: 'success',
795-
number: pull_request.number,
796-
link: pull_request.html_url,
809+
number,
810+
link: htmlUrl,
797811
webviewUri,
798812
llmDetails: head_ref ? `Local pending changes have been pushed to branch '${head_ref}'. ${prLlmString}` : prLlmString,
799-
sessionId: session_id
813+
sessionId: response.session_id
800814
};
801815
} catch (error) {
802816
return { error: error.message, state: 'error' };
@@ -1585,6 +1599,32 @@ export class CopilotRemoteAgentManager extends Disposable {
15851599
this._stateModel.clear();
15861600
}
15871601

1602+
private async waitForJobWithPullRequest(
1603+
capiClient: CopilotApi,
1604+
owner: string,
1605+
repo: string,
1606+
jobId: string,
1607+
token?: vscode.CancellationToken
1608+
): Promise<JobInfo | undefined> {
1609+
const maxWaitTime = 30 * 1000; // 30 seconds
1610+
const pollInterval = 2000; // 2 seconds
1611+
const startTime = Date.now();
1612+
1613+
Logger.appendLine(`Waiting for job ${jobId} to have pull request information...`, CopilotRemoteAgentManager.ID);
1614+
1615+
while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) {
1616+
const jobInfo = await capiClient.getJobByJobId(owner, repo, jobId);
1617+
if (jobInfo && jobInfo.pull_request && jobInfo.pull_request.number) {
1618+
Logger.appendLine(`Job ${jobId} now has pull request #${jobInfo.pull_request.number}`, CopilotRemoteAgentManager.ID);
1619+
return jobInfo;
1620+
}
1621+
await new Promise(resolve => setTimeout(resolve, pollInterval));
1622+
}
1623+
1624+
Logger.warn(`Timed out waiting for job ${jobId} to have pull request information`, CopilotRemoteAgentManager.ID);
1625+
return undefined;
1626+
}
1627+
15881628
public async cancelMostRecentChatSession(pullRequest: PullRequestModel): Promise<void> {
15891629
const capi = await this.copilotApi;
15901630
if (!capi) {

0 commit comments

Comments
 (0)