Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class MockOctoKitService implements IOctoKitService {
getUserOrganizations = async () => this.userOrganizations;
getOrganizationRepositories = async (org: string) => [org === 'testorg' ? 'testrepo' : 'repo'];
getCopilotAgentModels = async () => [];
getAssignableActors = async () => [];

async getCustomAgents(owner: string, repo: string, options: CustomAgentListOptions, authOptions: { createIfNone?: boolean }): Promise<CustomAgentListItem[]> {
if (!(await this.getCurrentAuthedUser())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ const ACTIVE_SESSION_POLL_INTERVAL_MS = 5 * 1000; // 5 seconds
const SEEN_DELEGATION_PROMPT_KEY = 'seenDelegationPromptBefore';

// TODO: No API from GH yet.
const HARDCODED_PARTNER_AGENTS: { id: string; name: string; at?: string }[] = [
{ id: DEFAULT_PARTNER_AGENT_ID, name: 'Copilot' },
{ id: '2246796', name: 'Claude', at: 'claude[agent]' },
{ id: '2248422', name: 'Codex', at: 'codex[agent]' }
const HARDCODED_PARTNER_AGENTS: { id: string; name: string; at?: string; assignableActorLogin?: string }[] = [
{ id: DEFAULT_PARTNER_AGENT_ID, name: 'Copilot', assignableActorLogin: 'copilot-swe-agent' },
{ id: '2246796', name: 'Claude', at: 'claude[agent]', assignableActorLogin: 'anthropic-code-agent' },
{ id: '2248422', name: 'Codex', at: 'codex[agent]', assignableActorLogin: 'openai-code-agent' }
];

/**
Expand Down Expand Up @@ -162,6 +162,8 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
private readonly plainTextRenderer = new PlainTextRenderer();
private readonly gitOperationsManager = new CopilotCloudGitOperationsManager(this.logService, this._gitService, this._gitExtensionService);

private _partnerAgentsAvailableCache: Map<string, { id: string; name: string; at?: string }[]> | undefined;

// Title
private TITLE = vscode.l10n.t('Delegate to cloud agent');

Expand Down Expand Up @@ -277,6 +279,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
this.cachedSessionItems = undefined;
this.activeSessionIds.clear();
this.stopActiveSessionPolling();
this._partnerAgentsAvailableCache = undefined;
this._onDidChangeChatSessionItems.fire();
}

Expand Down Expand Up @@ -356,24 +359,63 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
}
}

/**
* Queries for available partner agents by checking if known CCA logins are assignable in the repository.
* TODO: Remove once given a proper API
*/
private async getAvailablePartnerAgents(owner: string, repo: string): Promise<{ id: string; name: string; at?: string }[]> {
const cacheKey = `${owner}/${repo}`;

// Return cached result if available
if (this._partnerAgentsAvailableCache?.has(cacheKey)) {
return this._partnerAgentsAvailableCache.get(cacheKey)!;
}

try {
// Fetch assignable actors for the repository
const assignableActors = await this._octoKitService.getAssignableActors(owner, repo, { createIfNone: false });

// Check which agents from HARDCODED_PARTNER_AGENTS are assignable
const availableAgents: { id: string; name: string; at?: string }[] = [];

for (const agent of HARDCODED_PARTNER_AGENTS) {
const isAssignable = agent.assignableActorLogin && assignableActors.some(actor => actor.login === agent.assignableActorLogin);
if (isAssignable) {
availableAgents.push(agent);
}
}

if (!this._partnerAgentsAvailableCache) {
this._partnerAgentsAvailableCache = new Map();
}
this._partnerAgentsAvailableCache.set(cacheKey, availableAgents);

return availableAgents;
} catch (error) {
this.logService.error(`Error fetching partner agents: ${error}`);
return [];
}
}

async provideChatSessionProviderOptions(token: vscode.CancellationToken): Promise<vscode.ChatSessionProviderOptions> {
this.logService.trace('copilotCloudSessionsProvider#provideChatSessionProviderOptions Start');

const optionGroups: vscode.ChatSessionProviderOptionGroup[] = [];
const repoId = await getRepoId(this._gitService);

try {
// Fetch agents (requires repo) and models (global) in parallel
const [customAgents, models] = await Promise.all([
// Fetch agents (requires repo), models (global), and partner agents in parallel
const [customAgents, models, partnerAgents] = await Promise.all([
repoId ? this._octoKitService.getCustomAgents(repoId.org, repoId.repo, { excludeInvalidConfig: true }, { createIfNone: false }) : Promise.resolve([]),
this._octoKitService.getCopilotAgentModels({ createIfNone: false })
this._octoKitService.getCopilotAgentModels({ createIfNone: false }),
repoId ? this.getAvailablePartnerAgents(repoId.org, repoId.repo) : Promise.resolve([])
]);


// Partner agents
const partnerAgentsEnabled = this._configurationService.getConfig(ConfigKey.Advanced.CCAPartnerAgents);
if (partnerAgentsEnabled) {
const partnerAgentItems: vscode.ChatSessionProviderOptionItem[] = HARDCODED_PARTNER_AGENTS.map(agent => ({
if (partnerAgentsEnabled && partnerAgents.length > 0) {
const partnerAgentItems: vscode.ChatSessionProviderOptionItem[] = partnerAgents.map(agent => ({
id: agent.id,
name: agent.name,
}));
Expand Down
148 changes: 148 additions & 0 deletions src/platform/github/common/githubAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ export interface PullRequestComment {
url: string;
}

export interface AssignableActor {
__typename: string;
login: string;
avatarUrl?: string;
url?: string;
}

export interface AssignableActorsResponse {
repository: {
suggestedActors?: {
nodes: AssignableActor[];
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
};
assignableUsers?: {
nodes: AssignableActor[];
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
};
};
}

export async function makeGitHubAPIRequest(
fetcherService: IFetcherService,
logService: ILogService,
Expand Down Expand Up @@ -399,3 +425,125 @@ export async function makeGitHubAPIRequestWithPagination(

return sessionInfos;
}

/**
* Fetches assignable actors (users/bots) for a repository using suggestedActors API.
* This is the preferred API as it filters by capability (CAN_BE_ASSIGNED).
*/
export async function getAssignableActorsWithSuggestedActors(
fetcherService: IFetcherService,
logService: ILogService,
telemetry: ITelemetryService,
host: string,
token: string | undefined,
owner: string,
repo: string,
): Promise<AssignableActor[]> {
const query = `
query GetSuggestedActors($owner: String!, $name: String!, $first: Int!, $after: String) {
repository(owner: $owner, name: $name) {
suggestedActors(
first: $first
after: $after
capabilities: [CAN_BE_ASSIGNED]
) {
nodes {
__typename
login
avatarUrl
url
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`;

const actors: AssignableActor[] = [];
let after: string | null = null;
let hasNextPage = true;

while (hasNextPage) {
const variables = {
owner,
name: repo,
first: 100,
after,
};

const result = await makeGitHubGraphQLRequest(fetcherService, logService, telemetry, host, query, token, variables);

if (!result?.data?.repository?.suggestedActors) {
break;
}

const data = result.data.repository.suggestedActors;
actors.push(...data.nodes);
hasNextPage = data.pageInfo.hasNextPage;
after = data.pageInfo.endCursor;
}

return actors;
}

/**
* Fetches assignable users for a repository using assignableUsers API.
* This is a fallback for older GitHub Enterprise Server instances that don't support suggestedActors.
*/
export async function getAssignableActorsWithAssignableUsers(
fetcherService: IFetcherService,
logService: ILogService,
telemetry: ITelemetryService,
host: string,
token: string | undefined,
owner: string,
repo: string,
): Promise<AssignableActor[]> {
const query = `
query GetAssignableUsers($owner: String!, $name: String!, $first: Int!, $after: String) {
repository(owner: $owner, name: $name) {
assignableUsers(first: $first, after: $after) {
nodes {
__typename
login
avatarUrl
url
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`;

const actors: AssignableActor[] = [];
let after: string | null = null;
let hasNextPage = true;

while (hasNextPage) {
const variables = {
owner,
name: repo,
first: 100,
after,
};

const result = await makeGitHubGraphQLRequest(fetcherService, logService, telemetry, host, query, token, variables);

if (!result?.data?.repository?.assignableUsers) {
break;
}

const data = result.data.repository.assignableUsers;
actors.push(...data.nodes);
hasNextPage = data.pageInfo.hasNextPage;
after = data.pageInfo.endCursor;
}

return actors;
}
16 changes: 13 additions & 3 deletions src/platform/github/common/githubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient';
import { ILogService } from '../../log/common/logService';
import { IFetcherService } from '../../networking/common/fetcherService';
import { ITelemetryService } from '../../telemetry/common/telemetry';
import { addPullRequestCommentGraphQLRequest, closePullRequest, getPullRequestFromGlobalId, makeGitHubAPIRequest, makeSearchGraphQLRequest, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI';
import { addPullRequestCommentGraphQLRequest, AssignableActor, closePullRequest, getPullRequestFromGlobalId, makeGitHubAPIRequest, makeSearchGraphQLRequest, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI';

/**
* Options for controlling authentication behavior in OctoKit service methods.
Expand Down Expand Up @@ -349,6 +349,16 @@ export interface IOctoKitService {
* @returns An array of available models. The first model is always 'Auto' and should be the default.
*/
getCopilotAgentModels(authOptions: AuthOptions): Promise<CCAModel[]>;

/**
* Gets the list of assignable actors (users/bots) for a repository.
* This is used to check if partner agents like Copilot are available for assignment.
* @param owner The repository owner
* @param repo The repository name
* @param authOptions - Authentication options. By default, uses silent auth and returns empty array if not authenticated.
* @returns An array of assignable actors with their login names
*/
getAssignableActors(owner: string, repo: string, authOptions: AuthOptions): Promise<AssignableActor[]>;
}

/**
Expand All @@ -360,9 +370,9 @@ export interface IOctoKitService {
export class BaseOctoKitService {
constructor(
protected readonly _capiClientService: ICAPIClientService,
private readonly _fetcherService: IFetcherService,
protected readonly _fetcherService: IFetcherService,
protected readonly _logService: ILogService,
private readonly _telemetryService: ITelemetryService
protected readonly _telemetryService: ITelemetryService
) { }

async getCurrentAuthedUserWithToken(token: string): Promise<IOctoKitUser | undefined> {
Expand Down
42 changes: 41 additions & 1 deletion src/platform/github/common/octoKitServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient';
import { ILogService } from '../../log/common/logService';
import { IFetcherService } from '../../networking/common/fetcherService';
import { ITelemetryService } from '../../telemetry/common/telemetry';
import { PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI';
import { AssignableActor, getAssignableActorsWithAssignableUsers, getAssignableActorsWithSuggestedActors, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI';
import { BaseOctoKitService, CustomAgentDetails, CustomAgentListItem, CustomAgentListOptions, ErrorResponseWithStatusCode, IOctoKitService, IOctoKitUser, JobInfo, PermissiveAuthRequiredError, PullRequestFile, RemoteAgentJobResponse } from './githubService';

export class OctoKitService extends BaseOctoKitService implements IOctoKitService {
Expand Down Expand Up @@ -374,4 +374,44 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic
return [];
}
}

async getAssignableActors(owner: string, repo: string, authOptions: { createIfNone?: boolean }): Promise<AssignableActor[]> {
const auth = (await this._authService.getGitHubSession('any', authOptions.createIfNone ? { createIfNone: true } : { silent: true }));
if (!auth?.accessToken) {
this._logService.trace('No authentication token available for getAssignableActors');
return [];
}

try {
// Try suggestedActors first (preferred API)
const actors = await getAssignableActorsWithSuggestedActors(
this._fetcherService,
this._logService,
this._telemetryService,
this._capiClientService.dotcomAPIURL,
auth.accessToken,
owner,
repo
);

if (actors.length > 0) {
return actors;
}

// Fall back to assignableUsers for older GitHub Enterprise Server instances
this._logService.trace('Falling back to assignableUsers API');
return await getAssignableActorsWithAssignableUsers(
this._fetcherService,
this._logService,
this._telemetryService,
this._capiClientService.dotcomAPIURL,
auth.accessToken,
owner,
repo
);
} catch (e) {
this._logService.error(`Error fetching assignable actors: ${e}`);
return [];
}
}
}
Loading