mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-29 19:59:19 +01:00
Cloud Agent: Tasks API experimental setting and backend (#317206)
* Cloud Agent: Tasks API experimental setting and backend
Introduces a CloudAgentBackend seam with two implementations:
- JobsApiBackend wraps the existing sweagentd Jobs API (default, no behavior change).
- TaskApiBackend implements the new Mission Control Task API and currently uses a StubTaskApiClient pending CAPI routing.
Selection is controlled by the new 'github.copilot.chat.cloudAgentBackend.version' setting ('v1' = Jobs API, 'v2' = Task API). The setting is tagged experimental and defaults to 'v1', preserving existing behavior for legacy users.
* Review comments
* Address PR review feedback
- TaskArtifactPullData: add optional 'number' field separate from db id
- Add ListTaskEventsOptions for event-specific pagination/filters
- Relax sendFollowUp success check (only undefined is failure)
- Drop unused _configurationService field; read config locally in ctor
- Document that cloudAgentBackend.version requires reload
This commit is contained in:
@@ -3491,6 +3491,22 @@
|
||||
"experimental"
|
||||
]
|
||||
},
|
||||
"github.copilot.chat.cloudAgentBackend.version": {
|
||||
"type": "string",
|
||||
"default": "v1",
|
||||
"enum": [
|
||||
"v1",
|
||||
"v2"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"%github.copilot.config.cloudAgentBackend.version.v1%",
|
||||
"%github.copilot.config.cloudAgentBackend.version.v2%"
|
||||
],
|
||||
"markdownDescription": "%github.copilot.config.cloudAgentBackend.version%",
|
||||
"tags": [
|
||||
"experimental"
|
||||
]
|
||||
},
|
||||
"github.copilot.chat.switchAgent.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
||||
@@ -424,6 +424,9 @@
|
||||
"github.copilot.config.cli.remote.enabled": "Enable the /remote command for Copilot CLI sessions, allowing you to view and steer from GitHub.com and the GitHub mobile app.",
|
||||
"github.copilot.config.backgroundAgent.enabled": "Enable the Copilot CLI. When disabled, the Copilot CLI will not be available in 'Continue In' context menus.",
|
||||
"github.copilot.config.cloudAgent.enabled": "Enable the Cloud Agent. When disabled, the Cloud Agent will not be available in 'Continue In' context menus.",
|
||||
"github.copilot.config.cloudAgentBackend.version": "Selects which backend the Cloud Agent uses to create and manage cloud sessions. This setting is experimental and may change. Changes take effect after reloading the window.",
|
||||
"github.copilot.config.cloudAgentBackend.version.v1": "Use the legacy Jobs API backend.",
|
||||
"github.copilot.config.cloudAgentBackend.version.v2": "Use the new Mission Control Task API backend.",
|
||||
"github.copilot.config.gpt5AlternativePatch": "Enable GPT-5 alternative patch format.",
|
||||
"github.copilot.config.inlineEdits.triggerOnEditorChangeAfterSeconds": "Trigger inline edits after editor has been idle for this many seconds.",
|
||||
"github.copilot.config.inlineEdits.nextCursorPrediction.displayLine": "Display predicted cursor line for next edit suggestions.",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Wire-format types for the Mission Control Task API plus the {@link ITaskApiClient}
|
||||
* abstraction. The HTTP client implementation is not yet available — see
|
||||
* {@link StubTaskApiClient} in the `vscode-node/taskApiBackend.ts` companion file.
|
||||
*/
|
||||
|
||||
export type TaskState =
|
||||
| 'queued'
|
||||
| 'in_progress'
|
||||
| 'idle'
|
||||
| 'waiting_for_user'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'timed_out'
|
||||
| 'cancelled';
|
||||
|
||||
export interface TaskCreator {
|
||||
readonly id?: number;
|
||||
readonly login?: string;
|
||||
}
|
||||
|
||||
export interface TaskRepositoryOwner {
|
||||
readonly id?: number;
|
||||
readonly login?: string;
|
||||
}
|
||||
|
||||
export interface TaskRepository {
|
||||
readonly id?: number;
|
||||
readonly name?: string;
|
||||
readonly owner?: TaskRepositoryOwner;
|
||||
}
|
||||
|
||||
export interface TaskArtifactPullData {
|
||||
/** Database id of the pull request (not the user-facing number). */
|
||||
readonly id: number;
|
||||
/** User-facing pull request number (e.g. #42). */
|
||||
readonly number?: number;
|
||||
readonly global_id?: string;
|
||||
readonly html_url?: string;
|
||||
}
|
||||
|
||||
export interface TaskArtifact {
|
||||
readonly type: 'pull' | string;
|
||||
readonly data: TaskArtifactPullData | Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
readonly state: TaskState;
|
||||
readonly created_at: string;
|
||||
readonly updated_at?: string;
|
||||
readonly archived_at?: string | null;
|
||||
readonly html_url?: string;
|
||||
readonly creator?: TaskCreator;
|
||||
readonly repository?: TaskRepository;
|
||||
readonly artifacts?: readonly TaskArtifact[];
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
readonly prompt: string;
|
||||
readonly event_content?: string;
|
||||
readonly problem_statement?: string;
|
||||
readonly base_ref?: string;
|
||||
readonly head_ref?: string;
|
||||
readonly create_pull_request?: boolean;
|
||||
readonly event_type?: string;
|
||||
readonly custom_agent?: string;
|
||||
readonly model?: string;
|
||||
readonly agent_id?: number;
|
||||
}
|
||||
|
||||
export interface SteerTaskRequest {
|
||||
readonly content: string;
|
||||
readonly type: 'user_message' | string;
|
||||
}
|
||||
|
||||
export interface ListTasksOptions {
|
||||
readonly page?: number;
|
||||
readonly per_page?: number;
|
||||
}
|
||||
|
||||
export interface ListTaskEventsOptions {
|
||||
readonly page?: number;
|
||||
readonly per_page?: number;
|
||||
/** Return events created after this cursor (server-defined opaque value). */
|
||||
readonly after?: string;
|
||||
/** Return events created at or after this ISO-8601 timestamp. */
|
||||
readonly since?: string;
|
||||
/** Restrict to events of these types (e.g. `user.message`). */
|
||||
readonly type?: readonly string[];
|
||||
}
|
||||
|
||||
export interface ListTasksResponse {
|
||||
readonly tasks: readonly Task[];
|
||||
readonly total_count?: number;
|
||||
}
|
||||
|
||||
export interface TaskEvent {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
readonly created_at: string;
|
||||
readonly data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ListTaskEventsResponse {
|
||||
readonly events: readonly TaskEvent[];
|
||||
}
|
||||
|
||||
export interface CreatePullRequestForTaskResponse {
|
||||
readonly pull_request_id: number;
|
||||
readonly pull_request_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP client for the Mission Control Task API. The implementation lands in a
|
||||
* follow-up PR once CAPI routing is available — see {@link StubTaskApiClient}.
|
||||
*/
|
||||
export interface ITaskApiClient {
|
||||
createTask(owner: string, repo: string, request: CreateTaskRequest): Promise<Task>;
|
||||
listTasksForRepo(owner: string, repo: string, options?: ListTasksOptions): Promise<ListTasksResponse>;
|
||||
listTasks(options?: ListTasksOptions): Promise<ListTasksResponse>;
|
||||
getTask(taskId: string): Promise<Task>;
|
||||
getTaskEvents(taskId: string, options?: ListTaskEventsOptions): Promise<ListTaskEventsResponse>;
|
||||
steerTask(taskId: string, request: SteerTaskRequest): Promise<void>;
|
||||
createPRForTask(owner: string, repo: string, taskId: string): Promise<CreatePullRequestForTaskResponse>;
|
||||
archiveTask(owner: string, repo: string, taskId: string): Promise<Task>;
|
||||
unarchiveTask(owner: string, repo: string, taskId: string): Promise<Task>;
|
||||
}
|
||||
+80
-233
@@ -3,11 +3,11 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RemoteAgentJobPayload } from '@vscode/copilot-api';
|
||||
import * as pathLib from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { l10n, Uri } from 'vscode';
|
||||
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
|
||||
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
|
||||
import { IDomainService } from '../../../platform/endpoint/common/domainService';
|
||||
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
|
||||
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
|
||||
@@ -15,7 +15,7 @@ import { FileType } from '../../../platform/filesystem/common/fileTypes';
|
||||
import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';
|
||||
import { GithubRepoId, IGitService } from '../../../platform/git/common/gitService';
|
||||
import { derivePullRequestState, PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI';
|
||||
import { AuthOptions, CCAEnabledResult, IGithubRepositoryService, IOctoKitService, JobInfo, RemoteAgentJobResponse } from '../../../platform/github/common/githubService';
|
||||
import { AuthOptions, CCAEnabledResult, IGithubRepositoryService, IOctoKitService } from '../../../platform/github/common/githubService';
|
||||
import { ILogService } from '../../../platform/log/common/logService';
|
||||
import { emitCloudSessionInvokeEvent } from '../../../platform/otel/common/genAiEvents';
|
||||
import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics';
|
||||
@@ -31,9 +31,12 @@ import { IInstantiationService } from '../../../util/vs/platform/instantiation/c
|
||||
import { SingleSlotTtlCache, TtlCache } from '../common/ttlCache';
|
||||
import { isUntitledSessionId } from '../common/utils';
|
||||
import { IChatDelegationSummaryService } from '../copilotcli/common/delegationSummaryService';
|
||||
import { body_suffix, CONTINUE_TRUNCATION, extractTitle, formatBodyPlaceholder, getAuthorDisplayName, getRepoId, JOBS_API_VERSION, SessionIdForPr, toOpenPullRequestWebviewUri, truncatePrompt } from '../vscode/copilotCodingAgentUtils';
|
||||
import { CONTINUE_TRUNCATION, extractTitle, getAuthorDisplayName, getRepoId, SessionIdForPr, toOpenPullRequestWebviewUri, truncatePrompt } from '../vscode/copilotCodingAgentUtils';
|
||||
import { CloudAgentBackend } from '../vscode/cloudAgentBackend';
|
||||
import { CopilotCloudGitOperationsManager } from './copilotCloudGitOperationsManager';
|
||||
import { ChatSessionContentBuilder, SessionResponseLogChunk } from './copilotCloudSessionContentBuilder';
|
||||
import { JobsApiBackend } from './jobsApiBackend';
|
||||
import { StubTaskApiClient, TaskApiBackend } from './taskApiBackend';
|
||||
import { IPullRequestFileChangesService } from './pullRequestFileChangesService';
|
||||
import MarkdownIt = require('markdown-it');
|
||||
|
||||
@@ -304,6 +307,9 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
// Workspace storage keys
|
||||
private readonly WORKSPACE_CONTEXT_PREFIX = 'copilot.cloudAgent';
|
||||
|
||||
// Backend abstraction for Jobs API / Task API migration. Selection happens in the constructor.
|
||||
private readonly _backend: CloudAgentBackend;
|
||||
|
||||
constructor(
|
||||
@IOctoKitService private readonly _octoKitService: IOctoKitService,
|
||||
@IGitService private readonly _gitService: IGitService,
|
||||
@@ -320,8 +326,20 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
@IDomainService private readonly _domainService: IDomainService,
|
||||
@IOTelService private readonly _otelService: IOTelService,
|
||||
@IFileSystemService private readonly _fileSystemService: IFileSystemService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Select Cloud Agent backend based on the `chat.cloudAgentBackend.version` setting.
|
||||
// Default 'v1' keeps existing Jobs API behavior; 'v2' opts in to the experimental Task API backend.
|
||||
// Note: read once at construction — changes to the setting require an extension host reload to take effect.
|
||||
const backendVersion = configurationService.getConfig(ConfigKey.CloudAgentBackendVersion);
|
||||
if (backendVersion === 'v2') {
|
||||
this._backend = new TaskApiBackend(new StubTaskApiClient(this.logService), this.logService);
|
||||
} else {
|
||||
this._backend = new JobsApiBackend(this._octoKitService, this.logService, this.telemetry, this._otelService);
|
||||
}
|
||||
|
||||
this.registerCommands();
|
||||
|
||||
// Refresh when CAPI URL changes (e.g., when GHE Copilot token arrives and updates the base URL)
|
||||
@@ -348,8 +366,8 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
let intervalMs: number;
|
||||
let hasHistoricalSessions: boolean;
|
||||
try {
|
||||
const sessions = await Promise.all(repoIds.map(repoId => this._octoKitService.getAllSessions(`${repoId.org}/${repoId.repo}`, false, {})));
|
||||
hasHistoricalSessions = sessions.some(s => s.length > 0);
|
||||
const sessionList = await this._backend.fetchSessionList(repoIds, false, false);
|
||||
hasHistoricalSessions = sessionList.length > 0;
|
||||
intervalMs = this.getRefreshIntervalTime(hasHistoricalSessions);
|
||||
} catch (e) {
|
||||
this.logService.error(`Error during background refresh setup: ${e instanceof Error ? e.message : String(e)}`);
|
||||
@@ -360,11 +378,9 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
telemetryObj.intervalMs = intervalMs;
|
||||
telemetryObj.hasHistoricalSessions = hasHistoricalSessions;
|
||||
const schedulerCallback = async () => {
|
||||
let sessions = [];
|
||||
try {
|
||||
sessions = await Promise.all(repoIds.map(repoId => this._octoKitService.getAllSessions(`${repoId.org}/${repoId.repo}`, true, {})));
|
||||
sessions = sessions.flat();
|
||||
if (this.cachedSessionsSize !== sessions.length) {
|
||||
const sessionList = await this._backend.fetchSessionList(repoIds, false, true);
|
||||
if (this.cachedSessionsSize !== sessionList.length) {
|
||||
this.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -705,7 +721,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
// Fetch only the active sessions using allSettled to handle individual failures
|
||||
const sessionResults = await Promise.allSettled(
|
||||
Array.from(this.activeSessionIds).map(sessionId =>
|
||||
this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS)
|
||||
this._backend.getSessionInfo(sessionId)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1107,29 +1123,16 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
this.logService.debug('copilotCloudSessionsProvider#provideChatSessionItems: not a GitHub repo, returning empty');
|
||||
return [];
|
||||
}
|
||||
let sessions = [];
|
||||
if (vscode.workspace.isAgentSessionsWorkspace || !repoIds || repoIds.length === 0) {
|
||||
sessions = await this._octoKitService.getAllSessions(undefined, true, {});
|
||||
} else {
|
||||
sessions = (await Promise.all(repoIds.map(repo => this._octoKitService.getAllSessions(`${repo.org}/${repo.repo}`, true, {})))).flat();
|
||||
}
|
||||
this.logService.debug(`copilotCloudSessionsProvider#provideChatSessionItems: fetched ${sessions.length} sessions`);
|
||||
this.cachedSessionsSize = sessions.length;
|
||||
|
||||
// Group sessions by resource_id and keep only the latest per resource_id
|
||||
const latestSessionsMap = new Map<number, SessionInfo>();
|
||||
for (const session of sessions) {
|
||||
const existing = latestSessionsMap.get(session.resource_id);
|
||||
if (!existing || this.shouldPushSession(session, existing)) {
|
||||
latestSessionsMap.set(session.resource_id, session);
|
||||
}
|
||||
}
|
||||
const sessionList = await this._backend.fetchSessionList(repoIds, vscode.workspace.isAgentSessionsWorkspace, true);
|
||||
this.logService.debug(`copilotCloudSessionsProvider#provideChatSessionItems: fetched ${sessionList.length} grouped sessions`);
|
||||
this.cachedSessionsSize = sessionList.length;
|
||||
|
||||
// Track active sessions for background polling
|
||||
const newActiveSessionIds = new Set<string>();
|
||||
for (const session of latestSessionsMap.values()) {
|
||||
if (session.state === 'in_progress' || session.state === 'queued') {
|
||||
newActiveSessionIds.add(session.id);
|
||||
for (const entry of sessionList) {
|
||||
const s = entry.latestSession;
|
||||
if (s.state === 'in_progress' || s.state === 'queued') {
|
||||
newActiveSessionIds.add(s.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,21 +1144,6 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
this.stopActiveSessionPolling();
|
||||
}
|
||||
|
||||
// Fetch PRs for all unique resource_global_ids in parallel
|
||||
const uniqueGlobalIds = new Set(Array.from(latestSessionsMap.values()).map(s => s.resource_global_id));
|
||||
const prFetches = Array.from(uniqueGlobalIds).map(async globalId => {
|
||||
try {
|
||||
const pr = await this._octoKitService.getPullRequestFromGlobalId(globalId, {});
|
||||
return { globalId, pr };
|
||||
} catch (e) {
|
||||
this.logService.warn(`Failed to fetch PR for global ID ${globalId}: ${e instanceof Error ? e.message : String(e)}`);
|
||||
return { globalId, pr: null };
|
||||
}
|
||||
});
|
||||
const prResults = await Promise.all(prFetches);
|
||||
const prMap = new Map(prResults.filter(r => r.pr).map(r => [r.globalId, r.pr!]));
|
||||
this.logService.debug(`copilotCloudSessionsProvider#provideChatSessionItems: resolved ${prMap.size}/${uniqueGlobalIds.size} PRs from global IDs`);
|
||||
|
||||
const validateISOTimestamp = (date: string | undefined): number | undefined => {
|
||||
try {
|
||||
if (!date) {
|
||||
@@ -1168,12 +1156,13 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
} catch { }
|
||||
};
|
||||
|
||||
// Create session items from latest sessions
|
||||
const sessionItems = await Promise.all(Array.from(latestSessionsMap.values()).map(async sessionItem => {
|
||||
const pr = prMap.get(sessionItem.resource_global_id);
|
||||
// Create session items from grouped entries
|
||||
const sessionItems = await Promise.all(sessionList.map(async entry => {
|
||||
const pr = entry.pullRequest;
|
||||
if (!pr) {
|
||||
return undefined;
|
||||
}
|
||||
const sessionItem = entry.latestSession;
|
||||
|
||||
const multiDiffPart = await this._prFileChangesService.getFileChangesMultiDiffPart(pr);
|
||||
const changes = multiDiffPart?.value?.map(change => new vscode.ChatSessionChangedFile(
|
||||
@@ -1249,20 +1238,11 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
return hasGitHubRepo;
|
||||
}
|
||||
|
||||
private shouldPushSession(sessionItem: SessionInfo, existing: SessionInfo | undefined): boolean {
|
||||
if (!existing) {
|
||||
return true;
|
||||
}
|
||||
const existingDate = new Date(existing.last_updated_at);
|
||||
const newDate = new Date(sessionItem.last_updated_at);
|
||||
return newDate > existingDate;
|
||||
}
|
||||
|
||||
async provideChatSessionContent(resource: Uri, token: vscode.CancellationToken): Promise<vscode.ChatSession> {
|
||||
const indexedSessionId = SessionIdForPr.parse(resource);
|
||||
const identity = this._backend.parseSessionId(resource);
|
||||
let pullRequestNumber: number | undefined;
|
||||
if (indexedSessionId) {
|
||||
pullRequestNumber = indexedSessionId.prNumber;
|
||||
if (identity?.type === 'pr') {
|
||||
pullRequestNumber = identity.prNumber;
|
||||
}
|
||||
if (typeof pullRequestNumber === 'undefined') {
|
||||
pullRequestNumber = SessionIdForPr.parsePullRequestNumber(resource);
|
||||
@@ -1283,8 +1263,8 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
summaryReference.complete(undefined);
|
||||
return undefined;
|
||||
}
|
||||
const jobInfo = await this._octoKitService.getJobBySessionId(repoOwner, repoName, sessions[0].id, 'vscode-copilot-chat', CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
let prompt = jobInfo?.problem_statement || 'Initial Implementation';
|
||||
const content = await this._backend.fetchSessionContent(repoOwner, repoName, sessions);
|
||||
let prompt = content.initialPrompt || 'Initial Implementation';
|
||||
// When delegating, we append the summary to the prompt, & that can be very large and doesn't look great.
|
||||
// Turn the summary into a reference instead.
|
||||
const info = this._chatDelegationSummaryService.extractPrompt(sessions[0].id, prompt);
|
||||
@@ -1339,7 +1319,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
});
|
||||
|
||||
const sessionContentBuilder = new ChatSessionContentBuilder(CopilotCloudSessionsProvider.TYPE, this._gitService);
|
||||
const history = await sessionContentBuilder.buildSessionHistory(getProblemStatement(pr.repository.owner.login, pr.repository.name, sortedSessions), sortedSessions, pr, (sessionId: string) => this._octoKitService.getSessionLogs(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS), storedReferences);
|
||||
const history = await sessionContentBuilder.buildSessionHistory(getProblemStatement(pr.repository.owner.login, pr.repository.name, sortedSessions), sortedSessions, pr, (sessionId: string) => this._backend.getSessionLogs(sessionId), storedReferences);
|
||||
|
||||
// const selectedCustomAgent = undefined; /* TODO: Needs API to support this. */
|
||||
// const selectedModel = undefined; /* TODO: Needs API to support this. */
|
||||
@@ -1401,7 +1381,10 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
|
||||
private createActiveResponseCallback(pr: PullRequestSearchItem, sessionId: string): (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => Thenable<void> {
|
||||
return async (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => {
|
||||
await this.waitForQueuedToInProgress(sessionId, token);
|
||||
const ready = await this._backend.waitForSessionReady(sessionId, token);
|
||||
if (ready) {
|
||||
this.refresh();
|
||||
}
|
||||
return this.streamSessionLogs(stream, pr, sessionId, token);
|
||||
};
|
||||
}
|
||||
@@ -2201,14 +2184,14 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
}
|
||||
|
||||
// Get the specific session info
|
||||
const sessionInfo = await this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
const sessionInfo = await this._backend.getSessionInfo(sessionId);
|
||||
if (!sessionInfo || token.isCancellationRequested) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get session logs
|
||||
const logs = await this._octoKitService.getSessionLogs(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
const logs = await this._backend.getSessionLogs(sessionId);
|
||||
|
||||
// Check if session is still in progress
|
||||
if (sessionInfo.state !== 'in_progress') {
|
||||
@@ -2369,58 +2352,6 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForQueuedToInProgress(
|
||||
sessionId: string,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<SessionInfo | undefined> {
|
||||
let sessionInfo: SessionInfo | undefined;
|
||||
|
||||
const waitForQueuedMaxRetries = 3;
|
||||
const waitForQueuedDelay = 5_000; // 5 seconds
|
||||
|
||||
// Allow for a short delay before the session is marked as 'queued'
|
||||
let waitForQueuedCount = 0;
|
||||
do {
|
||||
sessionInfo = await this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (sessionInfo && sessionInfo.state === 'queued') {
|
||||
this.logService.trace('Queued session found');
|
||||
break;
|
||||
}
|
||||
if (waitForQueuedCount < waitForQueuedMaxRetries) {
|
||||
this.logService.trace('Session not yet queued, waiting...');
|
||||
await new Promise(resolve => setTimeout(resolve, waitForQueuedDelay));
|
||||
}
|
||||
++waitForQueuedCount;
|
||||
} while (waitForQueuedCount <= waitForQueuedMaxRetries && (!token || !token.isCancellationRequested));
|
||||
|
||||
if (!sessionInfo || sessionInfo.state !== 'queued') {
|
||||
if (sessionInfo?.state === 'in_progress') {
|
||||
this.logService.trace('Session already in progress');
|
||||
this.refresh();
|
||||
return sessionInfo;
|
||||
}
|
||||
// Failure
|
||||
this.logService.trace('Failed to find queued session');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxWaitTime = 2 * 60 * 1_000; // 2 minutes
|
||||
const pollInterval = 3_000; // 3 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logService.trace(`Session ${sessionInfo.id} is queued, waiting for transition to in_progress...`);
|
||||
while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) {
|
||||
const sessionInfo = await this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (sessionInfo?.state === 'in_progress') {
|
||||
this.logService.trace(`Session ${sessionInfo.id} now in progress.`);
|
||||
this.refresh();
|
||||
return sessionInfo;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
this.logService.error(`Timed out waiting for session ${sessionId} to transition from queued to in_progress.`);
|
||||
}
|
||||
|
||||
private async waitForNewSession(
|
||||
pullRequest: PullRequestSearchItem,
|
||||
stream: vscode.ChatResponseStream,
|
||||
@@ -2446,11 +2377,12 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
if (!waitForTransitionToInProgress) {
|
||||
return newSession;
|
||||
}
|
||||
const inProgressSession = await this.waitForQueuedToInProgress(newSession.id, token);
|
||||
const inProgressSession = await this._backend.waitForSessionReady(newSession.id, token);
|
||||
if (!inProgressSession) {
|
||||
stream.markdown(vscode.l10n.t('Timed out waiting for cloud agent to begin work. Please try again shortly.'));
|
||||
return;
|
||||
}
|
||||
this.refresh();
|
||||
return inProgressSession;
|
||||
}
|
||||
|
||||
@@ -2479,60 +2411,23 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
this.logService.error(`Could not find pull request #${pullRequestNumber}`);
|
||||
return;
|
||||
}
|
||||
const commentBody = `@${targetAgent} ${userPrompt} ${summary ? '\n\n' + summary : ''}`;
|
||||
|
||||
const commentResult = await this._octoKitService.addPullRequestComment(pr.id, commentBody, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
const commentResult = await this._backend.sendFollowUp(pr.id, userPrompt + (summary ? '\n\n' + summary : ''), targetAgent);
|
||||
if (!commentResult) {
|
||||
this.logService.error(`Failed to add comment to PR #${pullRequestNumber}`);
|
||||
return;
|
||||
}
|
||||
// allow-any-unicode-next-line
|
||||
return vscode.l10n.t('🚀 Follow-up comment added to [#{0}]({1})', pullRequestNumber, commentResult.url);
|
||||
return commentResult.url
|
||||
// allow-any-unicode-next-line
|
||||
? vscode.l10n.t('🚀 Follow-up comment added to [#{0}]({1})', pullRequestNumber, commentResult.url)
|
||||
// allow-any-unicode-next-line
|
||||
: vscode.l10n.t('🚀 Follow-up comment added to #{0}', pullRequestNumber);
|
||||
} catch (err) {
|
||||
this.logService.error(`Failed to add follow-up comment to PR #${pullRequestNumber}: ${err}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/github/sweagentd/blob/main/docs/adr/0001-create-job-api.md
|
||||
private validateRemoteAgentJobResponse(response: unknown): response is RemoteAgentJobResponse {
|
||||
return typeof response === 'object' && response !== null && 'job_id' in response && 'session_id' in response;
|
||||
}
|
||||
|
||||
private async waitForJobWithPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
jobId: string,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<JobInfo | undefined> {
|
||||
const maxWaitTime = 30 * 1000; // 30 seconds
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logService.trace(`Waiting for job ${jobId} to have pull request information...`);
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) {
|
||||
const jobInfo = await this._octoKitService.getJobByJobId(owner, repo, jobId, 'vscode-copilot-chat', CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (jobInfo && jobInfo.pull_request && jobInfo.pull_request.number) {
|
||||
/* __GDPR__
|
||||
"copilotcloud.chat.remoteAgentJobPullRequestReady" : {
|
||||
"owner": "joshspicer",
|
||||
"comment": "Event sent when a remote agent job first returns pull request information."
|
||||
}
|
||||
*/
|
||||
this.telemetry.sendMSFTTelemetryEvent('copilotcloud.chat.remoteAgentJobPullRequestReady');
|
||||
GenAiMetrics.incrementCloudPrReadyCount(this._otelService);
|
||||
this.logService.trace(`Job ${jobId} now has pull request #${jobInfo.pull_request.number}`);
|
||||
this.refresh();
|
||||
return jobInfo;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
|
||||
this.logService.warn(`Timed out waiting for job ${jobId} to have pull request information`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async invokeRemoteAgent(prompt: string, problemContext: string, token: vscode.CancellationToken, stream: vscode.ChatResponseStream, base_ref: string, head_ref?: string, customAgentName?: string, modelName?: string, partnerAgentName?: string, selectedRepository?: string): Promise<{ number: number; sessionId: string }> {
|
||||
const title = extractTitle(prompt, problemContext);
|
||||
const { problemStatement, isTruncated } = truncatePrompt(this.logService, prompt, problemContext);
|
||||
@@ -2583,85 +2478,37 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
|
||||
}
|
||||
}
|
||||
|
||||
const resolvePartnerAgentName = (partnerAgentName?: string): { agent_id?: number } => {
|
||||
this.logService.trace(`Resolving partner agent from: ${partnerAgentName}`);
|
||||
if (!partnerAgentName || partnerAgentName === DEFAULT_PARTNER_AGENT_ID) {
|
||||
return {};
|
||||
}
|
||||
// try convert to number
|
||||
let partnerAgentId: number | undefined;
|
||||
if (partnerAgentName && partnerAgentName !== DEFAULT_PARTNER_AGENT_ID) {
|
||||
const partnerAgentIdNum = Number(partnerAgentName);
|
||||
if (isNaN(partnerAgentIdNum)) {
|
||||
this.logService.warn(`Invalid partner agent name/id provided: ${partnerAgentName}`);
|
||||
return {};
|
||||
}
|
||||
return { agent_id: partnerAgentIdNum };
|
||||
};
|
||||
|
||||
const payload: RemoteAgentJobPayload = {
|
||||
problem_statement: problemStatement,
|
||||
event_content: prompt,
|
||||
event_type: 'visual_studio_code_remote_agent_tool_invoked',
|
||||
...(customAgentName && customAgentName !== DEFAULT_CUSTOM_AGENT_ID && { custom_agent: customAgentName }),
|
||||
...(modelName && modelName !== DEFAULT_MODEL_ID && { model: modelName }),
|
||||
...(resolvePartnerAgentName(partnerAgentName)),
|
||||
pull_request: {
|
||||
title,
|
||||
body_placeholder: formatBodyPlaceholder(title),
|
||||
base_ref,
|
||||
body_suffix,
|
||||
...(head_ref && { head_ref }),
|
||||
}
|
||||
};
|
||||
|
||||
/* __GDPR__
|
||||
"copilotcloud.chat.remoteAgentJobInvoke" : {
|
||||
"owner": "joshspicer",
|
||||
"comment": "Event sent when a remote agent job invocation starts.",
|
||||
"hasHeadRef": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether a head ref was provided for delegation." }
|
||||
}
|
||||
*/
|
||||
this.telemetry.sendMSFTTelemetryEvent('copilotcloud.chat.remoteAgentJobInvoke', {
|
||||
hasHeadRef: String(!!head_ref)
|
||||
});
|
||||
|
||||
stream?.progress(vscode.l10n.t('Delegating to cloud agent'));
|
||||
this.logService.debug(`[postCopilotAgentJob] Invoking cloud agent job with payload: ${JSON.stringify(payload)}`);
|
||||
const response = await this._octoKitService.postCopilotAgentJob(repoOwner, repoName, JOBS_API_VERSION, payload, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
this.logService.debug(`[postCopilotAgentJob] Received response from cloud agent job invocation: ${JSON.stringify(response)}`);
|
||||
if (!this.validateRemoteAgentJobResponse(response)) {
|
||||
const statusCode = response?.status;
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
throw new Error(vscode.l10n.t('Cloud agent is not authorized to run on this repository. This may be because the Copilot coding agent is disabled for your organization, or your active GitHub account does not have push access to the target repository.'));
|
||||
case 403:
|
||||
throw new Error(vscode.l10n.t('Cloud agent is not enabled for this repository. You may need to enable it in [GitHub settings]({0}) or contact your organization administrator.', `https://${repoHost}/settings/copilot/coding_agent`));
|
||||
case 404:
|
||||
throw new Error(vscode.l10n.t('The repository `{0}/{1}` was not found or you do not have access to it.', repoOwner, repoName));
|
||||
case 422:
|
||||
// NOTE: Although earlier checks should prevent this, ensure that if we end up
|
||||
// with a 422 from the API, we give a useful error message
|
||||
throw new Error(vscode.l10n.t('Cloud agent was unable to create a pull request with the specified base branch `{0}`. Please push the branch to the remote and verify repository rules allow this operation. For empty repos, push an initial commit and try again.', base_ref));
|
||||
case 500:
|
||||
throw new Error(vscode.l10n.t('Cloud agent service encountered an internal error. Please try again later.'));
|
||||
default:
|
||||
throw new Error(vscode.l10n.t('Received invalid response {0} from cloud agent.', statusCode ? statusCode : ''));
|
||||
} else {
|
||||
partnerAgentId = partnerAgentIdNum;
|
||||
}
|
||||
}
|
||||
|
||||
stream.progress(vscode.l10n.t('Creating pull request'));
|
||||
const jobInfo = await this.waitForJobWithPullRequest(repoOwner, repoName, response.job_id, token);
|
||||
const result = await this._backend.createSession({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
host: repoHost,
|
||||
title,
|
||||
prompt,
|
||||
problemStatement,
|
||||
baseRef: base_ref,
|
||||
headRef: head_ref,
|
||||
customAgent: customAgentName && customAgentName !== DEFAULT_CUSTOM_AGENT_ID ? customAgentName : undefined,
|
||||
model: modelName && modelName !== DEFAULT_MODEL_ID ? modelName : undefined,
|
||||
partnerAgentId,
|
||||
}, stream, token);
|
||||
|
||||
if (!jobInfo || !jobInfo.pull_request) {
|
||||
throw new Error(vscode.l10n.t('Failed to retrieve pull request information from job'));
|
||||
if (result.kind !== 'pullRequest') {
|
||||
// Task-shaped result: backend returned a task, but the current UI flow is PR-shaped.
|
||||
// Task UI rendering lands in a follow-up PR alongside the real Task API client.
|
||||
throw new Error(vscode.l10n.t('Task-shaped cloud sessions are not yet supported in this UI flow.'));
|
||||
}
|
||||
|
||||
const { number } = jobInfo.pull_request;
|
||||
if (!number || isNaN(number)) {
|
||||
throw new Error(vscode.l10n.t('Invalid pull request number received from cloud agent'));
|
||||
}
|
||||
return {
|
||||
number,
|
||||
sessionId: response.session_id
|
||||
};
|
||||
this.refresh();
|
||||
return { number: result.prNumber, sessionId: result.sessionId };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RemoteAgentJobPayload } from '@vscode/copilot-api';
|
||||
import * as vscode from 'vscode';
|
||||
import { l10n } from 'vscode';
|
||||
import { GithubRepoId } from '../../../platform/git/common/gitService';
|
||||
import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI';
|
||||
import { AuthOptions, IOctoKitService, JobInfo, RemoteAgentJobResponse } from '../../../platform/github/common/githubService';
|
||||
import { ILogService } from '../../../platform/log/common/logService';
|
||||
import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics';
|
||||
import { IOTelService } from '../../../platform/otel/common/otelService';
|
||||
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
|
||||
import {
|
||||
CloudAgentBackend,
|
||||
CloudDelegationResult,
|
||||
CloudSessionContent,
|
||||
CloudSessionData,
|
||||
CloudSessionIdentity,
|
||||
CreateCloudSessionParams,
|
||||
FollowUpResult,
|
||||
} from '../vscode/cloudAgentBackend';
|
||||
import { body_suffix, formatBodyPlaceholder, JOBS_API_VERSION, SessionIdForPr } from '../vscode/copilotCodingAgentUtils';
|
||||
|
||||
const CLOUD_SESSIONS_AUTH_OPTIONS: AuthOptions = { createIfNone: { detail: l10n.t('Sign in to GitHub to access Copilot cloud sessions.') } };
|
||||
|
||||
/**
|
||||
* Cloud agent backend backed by the legacy Jobs API. This is the default and is
|
||||
* behaviorally identical to the inline code that previously lived in
|
||||
* `CopilotCloudSessionsProvider`.
|
||||
*/
|
||||
export class JobsApiBackend implements CloudAgentBackend {
|
||||
|
||||
constructor(
|
||||
private readonly _octoKitService: IOctoKitService,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _telemetryService: ITelemetryService,
|
||||
private readonly _otelService: IOTelService,
|
||||
) { }
|
||||
|
||||
parseSessionId(resource: vscode.Uri): CloudSessionIdentity | undefined {
|
||||
const parsed = SessionIdForPr.parse(resource);
|
||||
if (parsed) {
|
||||
return { type: 'pr', prNumber: parsed.prNumber, sessionIndex: parsed.sessionIndex };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
|
||||
return this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
}
|
||||
|
||||
getSessionLogs(sessionId: string): Promise<string> {
|
||||
return this._octoKitService.getSessionLogs(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
}
|
||||
|
||||
async fetchSessionList(repoIds: GithubRepoId[] | undefined, isAgentWorkspace: boolean, refresh: boolean): Promise<CloudSessionData[]> {
|
||||
const sessions = await this.fetchAllSessions(repoIds, isAgentWorkspace, refresh);
|
||||
|
||||
// Group sessions by resource_id and keep only the latest per resource_id.
|
||||
const latestSessionsMap = new Map<number, SessionInfo>();
|
||||
for (const session of sessions) {
|
||||
const existing = latestSessionsMap.get(session.resource_id);
|
||||
if (!existing || new Date(session.last_updated_at) > new Date(existing.last_updated_at)) {
|
||||
latestSessionsMap.set(session.resource_id, session);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch PRs for all unique resource_global_ids in parallel.
|
||||
const uniqueGlobalIds = new Set(Array.from(latestSessionsMap.values()).map(s => s.resource_global_id));
|
||||
const prFetches = Array.from(uniqueGlobalIds).map(async globalId => {
|
||||
try {
|
||||
const pr = await this._octoKitService.getPullRequestFromGlobalId(globalId, {});
|
||||
return { globalId, pr };
|
||||
} catch (e) {
|
||||
this._logService.warn(`Failed to fetch PR for global ID ${globalId}: ${e instanceof Error ? e.message : String(e)}`);
|
||||
return { globalId, pr: null as PullRequestSearchItem | null };
|
||||
}
|
||||
});
|
||||
const prResults = await Promise.all(prFetches);
|
||||
const prMap = new Map(prResults.filter(r => r.pr).map(r => [r.globalId, r.pr!]));
|
||||
|
||||
return Array.from(latestSessionsMap.values()).map(latestSession => ({
|
||||
latestSession,
|
||||
pullRequest: prMap.get(latestSession.resource_global_id),
|
||||
}));
|
||||
}
|
||||
|
||||
async fetchSessionContent(repoOwner: string, repoName: string, sessions: SessionInfo[]): Promise<CloudSessionContent> {
|
||||
if (sessions.length === 0 || !repoOwner || !repoName) {
|
||||
return {};
|
||||
}
|
||||
const jobInfo = await this._octoKitService.getJobBySessionId(repoOwner, repoName, sessions[0].id, 'vscode-copilot-chat', CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
return { initialPrompt: jobInfo?.problem_statement || undefined };
|
||||
}
|
||||
|
||||
async sendFollowUp(pullRequestOrTaskId: string, prompt: string, targetAgent?: string): Promise<FollowUpResult | undefined> {
|
||||
const agent = targetAgent && targetAgent.length > 0 ? targetAgent : 'copilot';
|
||||
// Trailing space preserved for byte-identical bodies vs the pre-seam inline implementation.
|
||||
const body = `@${agent} ${prompt} `;
|
||||
const commentResult = await this._octoKitService.addPullRequestComment(pullRequestOrTaskId, body, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (!commentResult) {
|
||||
return undefined;
|
||||
}
|
||||
return { url: commentResult.url };
|
||||
}
|
||||
|
||||
async createSession(params: CreateCloudSessionParams, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<CloudDelegationResult> {
|
||||
const payload: RemoteAgentJobPayload = {
|
||||
problem_statement: params.problemStatement,
|
||||
event_content: params.prompt,
|
||||
event_type: 'visual_studio_code_remote_agent_tool_invoked',
|
||||
...(params.customAgent && { custom_agent: params.customAgent }),
|
||||
...(params.model && { model: params.model }),
|
||||
...(params.partnerAgentId !== undefined && { agent_id: params.partnerAgentId }),
|
||||
pull_request: {
|
||||
title: params.title,
|
||||
body_placeholder: formatBodyPlaceholder(params.title),
|
||||
base_ref: params.baseRef,
|
||||
body_suffix,
|
||||
...(params.headRef && { head_ref: params.headRef }),
|
||||
},
|
||||
};
|
||||
|
||||
/* __GDPR__
|
||||
"copilotcloud.chat.remoteAgentJobInvoke" : {
|
||||
"owner": "joshspicer",
|
||||
"comment": "Event sent when a remote agent job invocation starts.",
|
||||
"hasHeadRef": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether a head ref was provided for delegation." }
|
||||
}
|
||||
*/
|
||||
this._telemetryService.sendMSFTTelemetryEvent('copilotcloud.chat.remoteAgentJobInvoke', {
|
||||
hasHeadRef: String(!!params.headRef),
|
||||
});
|
||||
|
||||
stream?.progress(vscode.l10n.t('Delegating to cloud agent'));
|
||||
this._logService.debug(`[postCopilotAgentJob] Invoking cloud agent job with payload: ${JSON.stringify(payload)}`);
|
||||
const response = await this._octoKitService.postCopilotAgentJob(params.owner, params.repo, JOBS_API_VERSION, payload, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
this._logService.debug(`[postCopilotAgentJob] Received response from cloud agent job invocation: ${JSON.stringify(response)}`);
|
||||
if (!this.validateRemoteAgentJobResponse(response)) {
|
||||
const statusCode = (response as { status?: number } | undefined)?.status;
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
throw new Error(vscode.l10n.t('Cloud agent is not authorized to run on this repository. This may be because the Copilot coding agent is disabled for your organization, or your active GitHub account does not have push access to the target repository.'));
|
||||
case 403:
|
||||
throw new Error(vscode.l10n.t('Cloud agent is not enabled for this repository. You may need to enable it in [GitHub settings]({0}) or contact your organization administrator.', `https://${params.host}/settings/copilot/coding_agent`));
|
||||
case 404:
|
||||
throw new Error(vscode.l10n.t('The repository `{0}/{1}` was not found or you do not have access to it.', params.owner, params.repo));
|
||||
case 422:
|
||||
throw new Error(vscode.l10n.t('Cloud agent was unable to create a pull request with the specified base branch `{0}`. Please push the branch to the remote and verify repository rules allow this operation. For empty repos, push an initial commit and try again.', params.baseRef));
|
||||
case 500:
|
||||
throw new Error(vscode.l10n.t('Cloud agent service encountered an internal error. Please try again later.'));
|
||||
default:
|
||||
throw new Error(vscode.l10n.t('Received invalid response {0} from cloud agent.', statusCode ? statusCode : ''));
|
||||
}
|
||||
}
|
||||
|
||||
stream.progress(vscode.l10n.t('Creating pull request'));
|
||||
const jobInfo = await this.waitForJobWithPullRequest(params.owner, params.repo, response.job_id, token);
|
||||
|
||||
if (!jobInfo || !jobInfo.pull_request) {
|
||||
throw new Error(vscode.l10n.t('Failed to retrieve pull request information from job'));
|
||||
}
|
||||
|
||||
const { number } = jobInfo.pull_request;
|
||||
if (!number || isNaN(number)) {
|
||||
throw new Error(vscode.l10n.t('Invalid pull request number received from cloud agent'));
|
||||
}
|
||||
return { kind: 'pullRequest', prNumber: number, sessionId: response.session_id };
|
||||
}
|
||||
|
||||
async waitForSessionReady(sessionId: string, token?: vscode.CancellationToken): Promise<SessionInfo | undefined> {
|
||||
let sessionInfo: SessionInfo | undefined;
|
||||
|
||||
const waitForQueuedMaxRetries = 3;
|
||||
const waitForQueuedDelay = 5_000; // 5 seconds
|
||||
|
||||
let waitForQueuedCount = 0;
|
||||
do {
|
||||
sessionInfo = await this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (sessionInfo && sessionInfo.state === 'queued') {
|
||||
this._logService.trace('Queued session found');
|
||||
break;
|
||||
}
|
||||
if (waitForQueuedCount < waitForQueuedMaxRetries) {
|
||||
this._logService.trace('Session not yet queued, waiting...');
|
||||
await new Promise(resolve => setTimeout(resolve, waitForQueuedDelay));
|
||||
}
|
||||
++waitForQueuedCount;
|
||||
} while (waitForQueuedCount <= waitForQueuedMaxRetries && (!token || !token.isCancellationRequested));
|
||||
|
||||
if (!sessionInfo || sessionInfo.state !== 'queued') {
|
||||
if (sessionInfo?.state === 'in_progress') {
|
||||
this._logService.trace('Session already in progress');
|
||||
return sessionInfo;
|
||||
}
|
||||
this._logService.trace('Failed to find queued session');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxWaitTime = 2 * 60 * 1_000; // 2 minutes
|
||||
const pollInterval = 3_000; // 3 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
this._logService.trace(`Session ${sessionInfo.id} is queued, waiting for transition to in_progress...`);
|
||||
while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) {
|
||||
const info = await this._octoKitService.getSessionInfo(sessionId, CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (info?.state === 'in_progress') {
|
||||
this._logService.trace(`Session ${info.id} now in progress.`);
|
||||
return info;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
this._logService.error(`Timed out waiting for session ${sessionId} to transition from queued to in_progress.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// https://github.com/github/sweagentd/blob/main/docs/adr/0001-create-job-api.md
|
||||
private validateRemoteAgentJobResponse(response: unknown): response is RemoteAgentJobResponse {
|
||||
return typeof response === 'object' && response !== null && 'job_id' in response && 'session_id' in response;
|
||||
}
|
||||
|
||||
private async fetchAllSessions(repoIds: GithubRepoId[] | undefined, isAgentWorkspace: boolean, refresh: boolean): Promise<SessionInfo[]> {
|
||||
if (isAgentWorkspace || !repoIds || repoIds.length === 0) {
|
||||
return this._octoKitService.getAllSessions(undefined, refresh, {});
|
||||
}
|
||||
const all = await Promise.all(
|
||||
repoIds.map(repo => this._octoKitService.getAllSessions(`${repo.org}/${repo.repo}`, refresh, {})),
|
||||
);
|
||||
return all.flat();
|
||||
}
|
||||
|
||||
private async waitForJobWithPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
jobId: string,
|
||||
token?: vscode.CancellationToken,
|
||||
): Promise<JobInfo | undefined> {
|
||||
const maxWaitTime = 30 * 1000; // 30 seconds
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
this._logService.trace(`Waiting for job ${jobId} to have pull request information...`);
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) {
|
||||
const jobInfo = await this._octoKitService.getJobByJobId(owner, repo, jobId, 'vscode-copilot-chat', CLOUD_SESSIONS_AUTH_OPTIONS);
|
||||
if (jobInfo && jobInfo.pull_request && jobInfo.pull_request.number) {
|
||||
/* __GDPR__
|
||||
"copilotcloud.chat.remoteAgentJobPullRequestReady" : {
|
||||
"owner": "joshspicer",
|
||||
"comment": "Event sent when a remote agent job first returns pull request information."
|
||||
}
|
||||
*/
|
||||
this._telemetryService.sendMSFTTelemetryEvent('copilotcloud.chat.remoteAgentJobPullRequestReady');
|
||||
GenAiMetrics.incrementCloudPrReadyCount(this._otelService);
|
||||
this._logService.trace(`Job ${jobId} now has pull request #${jobInfo.pull_request.number}`);
|
||||
return jobInfo;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
|
||||
this._logService.warn(`Timed out waiting for job ${jobId} to have pull request information`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { GithubRepoId } from '../../../platform/git/common/gitService';
|
||||
import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI';
|
||||
import { ILogService } from '../../../platform/log/common/logService';
|
||||
import {
|
||||
CreatePullRequestForTaskResponse,
|
||||
CreateTaskRequest,
|
||||
ITaskApiClient,
|
||||
ListTaskEventsOptions,
|
||||
ListTaskEventsResponse,
|
||||
ListTasksOptions,
|
||||
ListTasksResponse,
|
||||
SteerTaskRequest,
|
||||
Task,
|
||||
TaskArtifact,
|
||||
TaskArtifactPullData,
|
||||
TaskState,
|
||||
} from '../common/taskApiTypes';
|
||||
import {
|
||||
CloudAgentBackend,
|
||||
CloudDelegationResult,
|
||||
CloudSessionContent,
|
||||
CloudSessionData,
|
||||
CloudSessionIdentity,
|
||||
CreateCloudSessionParams,
|
||||
FollowUpResult,
|
||||
} from '../vscode/cloudAgentBackend';
|
||||
import { extractTitle, SessionIdForPr, SessionIdForTask } from '../vscode/copilotCodingAgentUtils';
|
||||
|
||||
const TASK_SESSION_POLL_INTERVAL_MS = 2_000;
|
||||
const TASK_SESSION_POLL_TIMEOUT_MS = 60_000;
|
||||
|
||||
function mapTaskStateToSessionState(state: TaskState): SessionInfo['state'] {
|
||||
switch (state) {
|
||||
case 'queued':
|
||||
return 'queued';
|
||||
case 'in_progress':
|
||||
case 'idle':
|
||||
case 'waiting_for_user':
|
||||
return 'in_progress';
|
||||
case 'completed':
|
||||
return 'completed';
|
||||
case 'failed':
|
||||
case 'timed_out':
|
||||
case 'cancelled':
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
function findPullArtifact(task: Task): (TaskArtifact & { data: TaskArtifactPullData }) | undefined {
|
||||
return task.artifacts?.find(
|
||||
(a): a is TaskArtifact & { data: TaskArtifactPullData } =>
|
||||
a.type === 'pull' && typeof (a.data as TaskArtifactPullData).id === 'number',
|
||||
);
|
||||
}
|
||||
|
||||
/** Convert a Task into the SessionInfo shape the existing UI layer expects. */
|
||||
function taskToSessionInfo(task: Task): SessionInfo {
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.name ?? '',
|
||||
user_id: task.creator?.id ?? 0,
|
||||
agent_id: 0,
|
||||
logs: '',
|
||||
logs_blob_id: '',
|
||||
state: mapTaskStateToSessionState(task.state),
|
||||
owner_id: task.repository?.owner?.id ?? 0,
|
||||
repo_id: task.repository?.id ?? 0,
|
||||
resource_type: 'task',
|
||||
resource_id: 0,
|
||||
last_updated_at: task.updated_at ?? task.created_at,
|
||||
created_at: task.created_at,
|
||||
completed_at: task.state === 'completed' ? (task.updated_at ?? task.created_at) : '',
|
||||
event_type: 'task',
|
||||
workflow_run_id: 0,
|
||||
premium_requests: 0,
|
||||
error: task.state === 'failed' ? 'Task failed' : null,
|
||||
resource_global_id: '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Synthesize a PullRequestSearchItem from a task that has a PR artifact attached. */
|
||||
function taskToPullRequest(task: Task, pullArtifact: TaskArtifact & { data: TaskArtifactPullData }): PullRequestSearchItem {
|
||||
const prNumber = pullArtifact.data.number ?? pullArtifact.data.id;
|
||||
return {
|
||||
id: String(pullArtifact.data.global_id ?? pullArtifact.data.id),
|
||||
number: prNumber,
|
||||
title: task.name ?? '',
|
||||
state: 'OPEN',
|
||||
url: pullArtifact.data.html_url ?? task.html_url ?? '',
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at ?? task.created_at,
|
||||
author: task.creator ? { login: task.creator.login ?? '' } : null,
|
||||
repository: {
|
||||
owner: { login: task.repository?.owner?.login ?? '' },
|
||||
name: task.repository?.name ?? '',
|
||||
},
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
files: { totalCount: 0 },
|
||||
fullDatabaseId: pullArtifact.data.id,
|
||||
headRefOid: '',
|
||||
body: '',
|
||||
} as PullRequestSearchItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud agent backend backed by Mission Control's Task API.
|
||||
*
|
||||
* The shape of this class is reviewed and integrated against `CopilotCloudSessionsProvider`
|
||||
* today; the actual HTTP wire is not yet available — every call to {@link ITaskApiClient}
|
||||
* currently routes to {@link StubTaskApiClient} which throws. A follow-up PR will provide
|
||||
* a real client implementation backed by `@vscode/copilot-api`.
|
||||
*
|
||||
* Selected via the `github.copilot.chat.cloudAgentBackend.version` setting set to `v2`.
|
||||
*/
|
||||
export class TaskApiBackend implements CloudAgentBackend {
|
||||
|
||||
constructor(
|
||||
private readonly _taskApiClient: ITaskApiClient,
|
||||
private readonly _logService: ILogService,
|
||||
) { }
|
||||
|
||||
parseSessionId(resource: vscode.Uri): CloudSessionIdentity | undefined {
|
||||
const taskParsed = SessionIdForTask.parse(resource);
|
||||
if (taskParsed) {
|
||||
return { type: 'task', taskId: taskParsed.taskId };
|
||||
}
|
||||
// Fall back to PR parsing for backward compat with sessions created under v1.
|
||||
const prParsed = SessionIdForPr.parse(resource);
|
||||
if (prParsed) {
|
||||
return { type: 'pr', prNumber: prParsed.prNumber, sessionIndex: prParsed.sessionIndex };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async createSession(params: CreateCloudSessionParams, _stream: vscode.ChatResponseStream, _token: vscode.CancellationToken): Promise<CloudDelegationResult> {
|
||||
const request: CreateTaskRequest = {
|
||||
prompt: params.prompt,
|
||||
event_content: params.prompt,
|
||||
problem_statement: params.problemStatement,
|
||||
base_ref: params.baseRef,
|
||||
create_pull_request: true, // MVP — keeps PR-based UI working during transition
|
||||
event_type: 'visual_studio_code_remote_agent_tool_invoked',
|
||||
...(params.headRef && { head_ref: params.headRef }),
|
||||
...(params.customAgent && { custom_agent: params.customAgent }),
|
||||
...(params.model && { model: params.model }),
|
||||
...(params.partnerAgentId !== undefined && { agent_id: params.partnerAgentId }),
|
||||
};
|
||||
|
||||
const task = await this._taskApiClient.createTask(params.owner, params.repo, request);
|
||||
|
||||
// A task may already have a PR artifact attached on creation (unlikely but possible).
|
||||
const pullArtifact = findPullArtifact(task);
|
||||
if (pullArtifact) {
|
||||
return {
|
||||
kind: 'pullRequest',
|
||||
prNumber: pullArtifact.data.number ?? pullArtifact.data.id,
|
||||
sessionId: task.id,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'task',
|
||||
taskId: task.id,
|
||||
taskUrl: task.html_url ?? '',
|
||||
title: task.name ?? extractTitle(params.prompt, params.problemStatement) ?? params.title ?? 'Copilot task',
|
||||
sessionId: task.id,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchSessionList(repoIds: GithubRepoId[] | undefined, _isAgentWorkspace: boolean, _refresh: boolean): Promise<CloudSessionData[]> {
|
||||
const allTasks: Task[] = [];
|
||||
const listOpts: ListTasksOptions = { per_page: 100 };
|
||||
|
||||
if (!repoIds || repoIds.length === 0) {
|
||||
const response = await this._taskApiClient.listTasks(listOpts);
|
||||
allTasks.push(...response.tasks);
|
||||
} else {
|
||||
const responses = await Promise.all(
|
||||
repoIds.map(repo => this._taskApiClient.listTasksForRepo(repo.org, repo.repo, listOpts)
|
||||
.catch((e: unknown) => {
|
||||
this._logService.warn(`Failed to fetch tasks for ${repo.org}/${repo.repo}: ${e}`);
|
||||
return { tasks: [] as readonly Task[] } satisfies ListTasksResponse;
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const response of responses) {
|
||||
allTasks.push(...response.tasks);
|
||||
}
|
||||
}
|
||||
|
||||
const activeTasks = allTasks.filter(t => !t.archived_at);
|
||||
|
||||
return activeTasks.map(task => {
|
||||
const pullArtifact = findPullArtifact(task);
|
||||
const data: CloudSessionData = {
|
||||
latestSession: taskToSessionInfo(task),
|
||||
...(pullArtifact && { pullRequest: taskToPullRequest(task, pullArtifact) }),
|
||||
};
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchSessionContent(_repoOwner: string, _repoName: string, sessions: SessionInfo[]): Promise<CloudSessionContent> {
|
||||
if (sessions.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const taskId = sessions[0].id;
|
||||
try {
|
||||
const response = await this._taskApiClient.getTaskEvents(taskId, { per_page: 100 });
|
||||
const firstUserMessage = response.events.find(e => e.type === 'user.message');
|
||||
const content = firstUserMessage?.data['content'];
|
||||
return { initialPrompt: typeof content === 'string' ? content : undefined };
|
||||
} catch (e) {
|
||||
this._logService.warn(`Failed to fetch events for task ${taskId}: ${e}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async sendFollowUp(pullRequestOrTaskId: string, prompt: string, _targetAgent?: string): Promise<FollowUpResult | undefined> {
|
||||
try {
|
||||
await this._taskApiClient.steerTask(pullRequestOrTaskId, { content: prompt, type: 'user_message' });
|
||||
return {};
|
||||
} catch (e) {
|
||||
this._logService.error(`Failed to steer task ${pullRequestOrTaskId}: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
|
||||
try {
|
||||
const task = await this._taskApiClient.getTask(sessionId);
|
||||
return taskToSessionInfo(task);
|
||||
} catch (e) {
|
||||
this._logService.warn(`Failed to fetch task ${sessionId}: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getSessionLogs(sessionId: string): Promise<string> {
|
||||
try {
|
||||
const response = await this._taskApiClient.getTaskEvents(sessionId, { per_page: 100 });
|
||||
return JSON.stringify(response.events, undefined, 2);
|
||||
} catch (e) {
|
||||
this._logService.warn(`Failed to fetch events for task ${sessionId}: ${e}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async waitForSessionReady(sessionId: string, token?: vscode.CancellationToken): Promise<SessionInfo | undefined> {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < TASK_SESSION_POLL_TIMEOUT_MS && !(token?.isCancellationRequested)) {
|
||||
try {
|
||||
const task = await this._taskApiClient.getTask(sessionId);
|
||||
const state = task.state;
|
||||
if (state === 'in_progress' || state === 'completed' || state === 'failed'
|
||||
|| state === 'timed_out' || state === 'cancelled') {
|
||||
return taskToSessionInfo(task);
|
||||
}
|
||||
} catch (e) {
|
||||
this._logService.warn(`Failed to poll task ${sessionId}: ${e}`);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, TASK_SESSION_POLL_INTERVAL_MS));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder {@link ITaskApiClient} used until CAPI routing for the Task API is
|
||||
* available. Every method throws a localized "not yet wired" error so the
|
||||
* orchestration in {@link TaskApiBackend} is exercised end-to-end (and reviewed
|
||||
* in this PR) without needing a live wire.
|
||||
*/
|
||||
export class StubTaskApiClient implements ITaskApiClient {
|
||||
|
||||
constructor(private readonly _logService: ILogService) { }
|
||||
|
||||
private notWired(method: string): never {
|
||||
const msg = vscode.l10n.t('Task API client method `{0}` is not yet wired \u2014 awaiting CAPI routing.', method);
|
||||
this._logService.error(`StubTaskApiClient.${method}: ${msg}`);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
createTask(_owner: string, _repo: string, _request: CreateTaskRequest): Promise<Task> {
|
||||
this.notWired('createTask');
|
||||
}
|
||||
|
||||
listTasksForRepo(_owner: string, _repo: string, _options?: ListTasksOptions): Promise<ListTasksResponse> {
|
||||
this.notWired('listTasksForRepo');
|
||||
}
|
||||
|
||||
listTasks(_options?: ListTasksOptions): Promise<ListTasksResponse> {
|
||||
this.notWired('listTasks');
|
||||
}
|
||||
|
||||
getTask(_taskId: string): Promise<Task> {
|
||||
this.notWired('getTask');
|
||||
}
|
||||
|
||||
getTaskEvents(_taskId: string, _options?: ListTaskEventsOptions): Promise<ListTaskEventsResponse> {
|
||||
this.notWired('getTaskEvents');
|
||||
}
|
||||
|
||||
steerTask(_taskId: string, _request: SteerTaskRequest): Promise<void> {
|
||||
this.notWired('steerTask');
|
||||
}
|
||||
|
||||
createPRForTask(_owner: string, _repo: string, _taskId: string): Promise<CreatePullRequestForTaskResponse> {
|
||||
this.notWired('createPRForTask');
|
||||
}
|
||||
|
||||
archiveTask(_owner: string, _repo: string, _taskId: string): Promise<Task> {
|
||||
this.notWired('archiveTask');
|
||||
}
|
||||
|
||||
unarchiveTask(_owner: string, _repo: string, _taskId: string): Promise<Task> {
|
||||
this.notWired('unarchiveTask');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { GithubRepoId } from '../../../platform/git/common/gitService';
|
||||
import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI';
|
||||
|
||||
/**
|
||||
* Identifies a cloud session from a VS Code URI. Two shapes are supported so the
|
||||
* provider can transition from Jobs API (PR-based) to Task API (task-based).
|
||||
*/
|
||||
export type CloudSessionIdentity =
|
||||
| { type: 'pr'; prNumber: number; sessionIndex?: number }
|
||||
| { type: 'task'; taskId: string };
|
||||
|
||||
/**
|
||||
* Result of creating a cloud session. The provider decides how to render this.
|
||||
* Jobs API yields a PR + session id. Task API yields a task id + url + title.
|
||||
*/
|
||||
export type CloudDelegationResult =
|
||||
| { kind: 'pullRequest'; prNumber: number; sessionId: string }
|
||||
| { kind: 'task'; taskId: string; taskUrl: string; title: string; sessionId: string };
|
||||
|
||||
/**
|
||||
* Parameters for creating a new cloud session. The provider resolves UI sentinels
|
||||
* (default agent/model/repo) and performs CCA enablement / truncation UI before
|
||||
* calling the backend; values here are ready to be sent to the API.
|
||||
*/
|
||||
export interface CreateCloudSessionParams {
|
||||
readonly owner: string;
|
||||
readonly repo: string;
|
||||
readonly host: string;
|
||||
readonly title: string | undefined;
|
||||
readonly prompt: string;
|
||||
readonly problemStatement: string;
|
||||
readonly baseRef: string;
|
||||
readonly headRef?: string;
|
||||
readonly customAgent?: string;
|
||||
readonly model?: string;
|
||||
readonly partnerAgentId?: number;
|
||||
}
|
||||
|
||||
export interface FollowUpResult {
|
||||
readonly url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A grouped session entry returned by {@link CloudAgentBackend.fetchSessionList}.
|
||||
* For Jobs API this represents the latest session per pull request. For Task API
|
||||
* this represents a single task (one task = one session).
|
||||
*/
|
||||
export interface CloudSessionData {
|
||||
readonly latestSession: SessionInfo;
|
||||
readonly pullRequest?: PullRequestSearchItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content of a single cloud session — initial prompt extracted from API-specific
|
||||
* places (Jobs API `problem_statement` field; Task API first `user.message` event).
|
||||
*/
|
||||
export interface CloudSessionContent {
|
||||
readonly initialPrompt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstraction over the cloud agent service. `JobsApiBackend` talks to the legacy
|
||||
* Jobs API and is the default. `TaskApiBackend` is reserved for the Mission Control
|
||||
* Task API and currently throws on every API call.
|
||||
*/
|
||||
export interface CloudAgentBackend {
|
||||
/**
|
||||
* Fetch a grouped, UI-ready session list. The backend dedupes by resource and
|
||||
* resolves the associated pull request (or synthesizes one from task data).
|
||||
* @param refresh When true, bypass any client-side caches and fetch fresh data.
|
||||
*/
|
||||
fetchSessionList(
|
||||
repoIds: GithubRepoId[] | undefined,
|
||||
isAgentWorkspace: boolean,
|
||||
refresh: boolean,
|
||||
): Promise<CloudSessionData[]>;
|
||||
|
||||
/** Fetch initial prompt / problem statement for a session group. */
|
||||
fetchSessionContent(
|
||||
repoOwner: string,
|
||||
repoName: string,
|
||||
sessions: SessionInfo[],
|
||||
): Promise<CloudSessionContent>;
|
||||
|
||||
/** Create a new cloud session. */
|
||||
createSession(
|
||||
params: CreateCloudSessionParams,
|
||||
stream: vscode.ChatResponseStream,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<CloudDelegationResult>;
|
||||
|
||||
/**
|
||||
* Post a follow-up message against an existing session.
|
||||
* @param pullRequestOrTaskId Either a PR global id (Jobs API) or a task id (Task API).
|
||||
* @param prompt The raw user prompt; the backend formats it for the underlying API.
|
||||
* @param targetAgent Optional `@`-mention target (Jobs API only).
|
||||
*/
|
||||
sendFollowUp(
|
||||
pullRequestOrTaskId: string,
|
||||
prompt: string,
|
||||
targetAgent?: string,
|
||||
): Promise<FollowUpResult | undefined>;
|
||||
|
||||
/** Get the current status of a session. */
|
||||
getSessionInfo(sessionId: string): Promise<SessionInfo | undefined>;
|
||||
|
||||
/** Get the raw logs for a session (used for streaming and history rebuild). */
|
||||
getSessionLogs(sessionId: string): Promise<string>;
|
||||
|
||||
/** Block until the session has transitioned out of `queued`. */
|
||||
waitForSessionReady(
|
||||
sessionId: string,
|
||||
token?: vscode.CancellationToken,
|
||||
): Promise<SessionInfo | undefined>;
|
||||
|
||||
/** Parse a session URI into a {@link CloudSessionIdentity}. */
|
||||
parseSessionId(resource: vscode.Uri): CloudSessionIdentity | undefined;
|
||||
}
|
||||
@@ -119,6 +119,25 @@ export namespace SessionIdForPr {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SessionIdForTask {
|
||||
|
||||
export function getId(taskId: string): string {
|
||||
return `task/${taskId}`;
|
||||
}
|
||||
|
||||
export function parse(resource: vscode.Uri): { taskId: string } | undefined {
|
||||
const match = resource.path.match(/^\/task\/(.+)$/);
|
||||
if (match) {
|
||||
return { taskId: match[1] };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseTaskId(resource: vscode.Uri): string | undefined {
|
||||
return parse(resource)?.taskId;
|
||||
}
|
||||
}
|
||||
|
||||
export async function toOpenPullRequestWebviewUri(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
|
||||
@@ -1044,6 +1044,7 @@ export namespace ConfigKey {
|
||||
|
||||
export const BackgroundAgentEnabled = defineSetting<boolean>('chat.backgroundAgent.enabled', ConfigType.Simple, true);
|
||||
export const CloudAgentEnabled = defineSetting<boolean>('chat.cloudAgent.enabled', ConfigType.Simple, true);
|
||||
export const CloudAgentBackendVersion = defineSetting<'v1' | 'v2'>('chat.cloudAgentBackend.version', ConfigType.Simple, 'v1');
|
||||
export const AdditionalReadAccessPaths = defineSetting<string[]>('chat.additionalReadAccessPaths', ConfigType.Simple, []);
|
||||
export const SwitchAgentEnabled = defineSetting<boolean>('chat.switchAgent.enabled', ConfigType.ExperimentBased, false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user