mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-08 15:55:15 +01:00
Add support for GH Custom Agents (#278251)
This commit is contained in:
@@ -26,6 +26,7 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde
|
||||
import { IChatWidgetService } from '../../contrib/chat/browser/chat.js';
|
||||
import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js';
|
||||
import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js';
|
||||
import { ICustomAgentQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js';
|
||||
import { IChatModel } from '../../contrib/chat/common/chatModel.js';
|
||||
import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js';
|
||||
@@ -96,6 +97,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
|
||||
private readonly _chatRelatedFilesProviders = this._register(new DisposableMap<number, IDisposable>());
|
||||
|
||||
private readonly _customAgentsProviders = this._register(new DisposableMap<number, IDisposable>());
|
||||
private readonly _customAgentsProviderEmitters = this._register(new DisposableMap<number, Emitter<void>>());
|
||||
|
||||
private readonly _pendingProgress = new Map<string, { progress: (parts: IChatProgress[]) => void; chatSession: IChatModel | undefined }>();
|
||||
private readonly _proxy: ExtHostChatAgentsShape2;
|
||||
|
||||
@@ -115,6 +119,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IExtensionService private readonly _extensionService: IExtensionService,
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
@IPromptsService private readonly _promptsService: IPromptsService,
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
|
||||
@@ -427,6 +432,46 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
$unregisterRelatedFilesProvider(handle: number): void {
|
||||
this._chatRelatedFilesProviders.deleteAndDispose(handle);
|
||||
}
|
||||
|
||||
async $registerCustomAgentsProvider(handle: number, extensionId: ExtensionIdentifier): Promise<void> {
|
||||
const extension = await this._extensionService.getExtension(extensionId.value);
|
||||
if (!extension) {
|
||||
this._logService.error(`[MainThreadChatAgents2] Could not find extension for CustomAgentsProvider: ${extensionId.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const emitter = new Emitter<void>();
|
||||
this._customAgentsProviderEmitters.set(handle, emitter);
|
||||
|
||||
const disposable = this._promptsService.registerCustomAgentsProvider(extension, {
|
||||
onDidChangeCustomAgents: emitter.event,
|
||||
provideCustomAgents: async (options: ICustomAgentQueryOptions, token: CancellationToken) => {
|
||||
const agents = await this._proxy.$provideCustomAgents(handle, options, token);
|
||||
if (!agents) {
|
||||
return undefined;
|
||||
}
|
||||
// Convert UriComponents to URI
|
||||
return agents.map(agent => ({
|
||||
...agent,
|
||||
uri: URI.revive(agent.uri)
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
this._customAgentsProviders.set(handle, disposable);
|
||||
}
|
||||
|
||||
$unregisterCustomAgentsProvider(handle: number): void {
|
||||
this._customAgentsProviders.deleteAndDispose(handle);
|
||||
this._customAgentsProviderEmitters.deleteAndDispose(handle);
|
||||
}
|
||||
|
||||
$onDidChangeCustomAgents(handle: number): void {
|
||||
const emitter = this._customAgentsProviderEmitters.get(handle);
|
||||
if (emitter) {
|
||||
emitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1541,6 +1541,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension, 'chatContextProvider');
|
||||
return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider);
|
||||
},
|
||||
registerCustomAgentsProvider(provider: vscode.CustomAgentsProvider): vscode.Disposable {
|
||||
checkProposedApiEnabled(extension, 'chatParticipantPrivate');
|
||||
return extHostChatAgents2.registerCustomAgentsProvider(extension, provider);
|
||||
},
|
||||
};
|
||||
|
||||
// namespace: lm
|
||||
@@ -1942,7 +1946,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition,
|
||||
McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition,
|
||||
McpToolAvailability: extHostTypes.McpToolAvailability,
|
||||
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind
|
||||
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind,
|
||||
CustomAgentTarget: extHostTypes.CustomAgentTarget,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariabl
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js';
|
||||
import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js';
|
||||
import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js';
|
||||
import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js';
|
||||
import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js';
|
||||
@@ -1393,6 +1394,9 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi
|
||||
$unregisterChatParticipantDetectionProvider(handle: number): void;
|
||||
$registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFilesProviderMetadata): void;
|
||||
$unregisterRelatedFilesProvider(handle: number): void;
|
||||
$registerCustomAgentsProvider(handle: number, extension: ExtensionIdentifier): void;
|
||||
$unregisterCustomAgentsProvider(handle: number): void;
|
||||
$onDidChangeCustomAgents(handle: number): void;
|
||||
$registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void;
|
||||
$unregisterAgentCompletionsProvider(handle: number, id: string): void;
|
||||
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;
|
||||
@@ -1457,6 +1461,7 @@ export interface ExtHostChatAgentsShape2 {
|
||||
$releaseSession(sessionId: string): void;
|
||||
$detectChatParticipant(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise<IChatParticipantDetectionResult | null | undefined>;
|
||||
$provideRelatedFiles(handle: number, request: Dto<IChatRequestDraft>, token: CancellationToken): Promise<Dto<IChatRelatedFile>[] | undefined>;
|
||||
$provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise<Dto<IExternalCustomAgent>[] | undefined>;
|
||||
$setRequestTools(requestId: string, tools: UserSelectedTools): void;
|
||||
}
|
||||
export interface IChatParticipantMetadata {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js';
|
||||
import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js';
|
||||
import * as typeConvert from './extHostTypeConverters.js';
|
||||
import * as extHostTypes from './extHostTypes.js';
|
||||
import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
|
||||
export class ChatAgentResponseStream {
|
||||
|
||||
@@ -395,6 +396,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
private static _relatedFilesProviderIdPool = 0;
|
||||
private readonly _relatedFilesProviders = new Map<number, ExtHostRelatedFilesProvider>();
|
||||
|
||||
private static _customAgentsProviderIdPool = 0;
|
||||
private readonly _customAgentsProviders = new Map<number, { extension: IExtensionDescription; provider: vscode.CustomAgentsProvider }>();
|
||||
|
||||
private readonly _sessionDisposables: DisposableMap<string, DisposableStore> = this._register(new DisposableMap());
|
||||
private readonly _completionDisposables: DisposableMap<number, DisposableStore> = this._register(new DisposableMap());
|
||||
|
||||
@@ -472,6 +476,28 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
});
|
||||
}
|
||||
|
||||
registerCustomAgentsProvider(extension: IExtensionDescription, provider: vscode.CustomAgentsProvider): vscode.Disposable {
|
||||
const handle = ExtHostChatAgents2._customAgentsProviderIdPool++;
|
||||
this._customAgentsProviders.set(handle, { extension, provider });
|
||||
this._proxy.$registerCustomAgentsProvider(handle, extension.identifier);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Listen to provider change events and notify main thread
|
||||
if (provider.onDidChangeCustomAgents) {
|
||||
disposables.add(provider.onDidChangeCustomAgents(() => {
|
||||
this._proxy.$onDidChangeCustomAgents(handle);
|
||||
}));
|
||||
}
|
||||
|
||||
disposables.add(toDisposable(() => {
|
||||
this._customAgentsProviders.delete(handle);
|
||||
this._proxy.$unregisterCustomAgentsProvider(handle);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
async $provideRelatedFiles(handle: number, request: IChatRequestDraft, token: CancellationToken): Promise<Dto<IChatRelatedFile>[] | undefined> {
|
||||
const provider = this._relatedFilesProviders.get(handle);
|
||||
if (!provider) {
|
||||
@@ -482,6 +508,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined;
|
||||
}
|
||||
|
||||
async $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise<IExternalCustomAgent[] | undefined> {
|
||||
const providerData = this._customAgentsProviders.get(handle);
|
||||
if (!providerData) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return await providerData.provider.provideCustomAgents(options, token) ?? undefined;
|
||||
}
|
||||
|
||||
async $detectChatParticipant(handle: number, requestDto: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise<vscode.ChatParticipantDetectionResult | null | undefined> {
|
||||
const detector = this._participantDetectionProviders.get(handle);
|
||||
if (!detector) {
|
||||
|
||||
@@ -3492,6 +3492,11 @@ export enum ChatErrorLevel {
|
||||
Error = 2
|
||||
}
|
||||
|
||||
export enum CustomAgentTarget {
|
||||
GitHubCopilot = 'github-copilot',
|
||||
VSCode = 'vscode',
|
||||
}
|
||||
|
||||
export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage {
|
||||
|
||||
static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { IProductService } from '../../../../../platform/product/common/productS
|
||||
import { IChatAgentService } from '../../common/chatAgents.js';
|
||||
import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
|
||||
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { ExtensionAgentSourceType, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { getOpenChatActionIdForMode } from '../actions/chatActions.js';
|
||||
import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../actions/chatExecuteActions.js';
|
||||
|
||||
@@ -104,7 +104,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
|
||||
const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id);
|
||||
const customModes = groupBy(
|
||||
modes.custom,
|
||||
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId ?
|
||||
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId && mode.source.type === ExtensionAgentSourceType.contribution ?
|
||||
'builtin' : 'custom');
|
||||
|
||||
const customBuiltinModeActions = customModes.builtin?.map(mode => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IChatAgentService } from './chatAgents.js';
|
||||
import { ChatContextKeys } from './chatContextKeys.js';
|
||||
import { ChatConfiguration, ChatModeKind } from './constants.js';
|
||||
import { IHandOff } from './promptSyntax/promptFileParser.js';
|
||||
import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
|
||||
import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
|
||||
|
||||
export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
|
||||
export interface IChatModeService {
|
||||
@@ -419,7 +419,7 @@ export class CustomChatMode implements IChatMode {
|
||||
}
|
||||
|
||||
type IChatModeSourceData =
|
||||
| { readonly storage: PromptsStorage.extension; readonly extensionId: string }
|
||||
| { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: ExtensionAgentSourceType }
|
||||
| { readonly storage: PromptsStorage.local | PromptsStorage.user };
|
||||
|
||||
function isChatModeSourceData(value: unknown): value is IChatModeSourceData {
|
||||
@@ -438,7 +438,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou
|
||||
return undefined;
|
||||
}
|
||||
if (source.storage === PromptsStorage.extension) {
|
||||
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value };
|
||||
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type };
|
||||
}
|
||||
return { storage: source.storage };
|
||||
}
|
||||
@@ -448,7 +448,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour
|
||||
return undefined;
|
||||
}
|
||||
if (data.storage === PromptsStorage.extension) {
|
||||
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) };
|
||||
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type: data.type ?? ExtensionAgentSourceType.contribution };
|
||||
}
|
||||
return { storage: data.storage };
|
||||
}
|
||||
|
||||
@@ -15,6 +15,44 @@ import { PromptsType } from '../promptTypes.js';
|
||||
import { IHandOff, ParsedPromptFile } from '../promptFileParser.js';
|
||||
import { ResourceSet } from '../../../../../../base/common/map.js';
|
||||
|
||||
/**
|
||||
* Target environment for custom agents.
|
||||
*/
|
||||
export enum CustomAgentTarget {
|
||||
GitHubCopilot = 'github-copilot',
|
||||
VSCode = 'vscode',
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for querying custom agents.
|
||||
*/
|
||||
export interface ICustomAgentQueryOptions {
|
||||
/**
|
||||
* Filter agents by target environment.
|
||||
*/
|
||||
readonly target?: CustomAgentTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom agent resource from an external provider.
|
||||
*/
|
||||
export interface IExternalCustomAgent {
|
||||
/**
|
||||
* The unique identifier/name of the custom agent resource.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* A description of what the custom agent resource does.
|
||||
*/
|
||||
readonly description: string;
|
||||
|
||||
/**
|
||||
* The URI to the agent or prompt resource file.
|
||||
*/
|
||||
readonly uri: URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides prompt services.
|
||||
*/
|
||||
@@ -29,6 +67,14 @@ export enum PromptsStorage {
|
||||
extension = 'extension'
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of source for extension agents.
|
||||
*/
|
||||
export enum ExtensionAgentSourceType {
|
||||
contribution = 'contribution',
|
||||
provider = 'provider',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a prompt path with its type.
|
||||
* This is used for both prompt files and prompt source folders.
|
||||
@@ -67,6 +113,7 @@ export interface IExtensionPromptPath extends IPromptPathBase {
|
||||
readonly extension: IExtensionDescription;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly source: ExtensionAgentSourceType;
|
||||
}
|
||||
export interface ILocalPromptPath extends IPromptPathBase {
|
||||
readonly storage: PromptsStorage.local;
|
||||
@@ -78,6 +125,7 @@ export interface IUserPromptPath extends IPromptPathBase {
|
||||
export type IAgentSource = {
|
||||
readonly storage: PromptsStorage.extension;
|
||||
readonly extensionId: ExtensionIdentifier;
|
||||
readonly type: ExtensionAgentSourceType;
|
||||
} | {
|
||||
readonly storage: PromptsStorage.local | PromptsStorage.user;
|
||||
};
|
||||
@@ -270,6 +318,18 @@ export interface IPromptsService extends IDisposable {
|
||||
*/
|
||||
setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void;
|
||||
|
||||
/**
|
||||
* Registers a CustomAgentsProvider that can provide custom agents for repositories.
|
||||
* This is part of the proposed API and requires the chatParticipantPrivate proposal.
|
||||
* @param extension The extension registering the provider.
|
||||
* @param provider The provider implementation with optional change event.
|
||||
* @returns A disposable that unregisters the provider when disposed.
|
||||
*/
|
||||
registerCustomAgentsProvider(extension: IExtensionDescription, provider: {
|
||||
onDidChangeCustomAgents?: Event<void>;
|
||||
provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise<IExternalCustomAgent[] | undefined>;
|
||||
}): IDisposable;
|
||||
|
||||
/**
|
||||
* Gets list of claude skills files.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
|
||||
import { CancellationError } from '../../../../../../base/common/errors.js';
|
||||
import { Emitter, Event } from '../../../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js';
|
||||
import { dirname, isEqual } from '../../../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
@@ -17,6 +17,7 @@ import { localize } from '../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js';
|
||||
import { IFileService } from '../../../../../../platform/files/common/files.js';
|
||||
import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
|
||||
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILabelService } from '../../../../../../platform/label/common/label.js';
|
||||
import { ILogService } from '../../../../../../platform/log/common/log.js';
|
||||
@@ -29,10 +30,9 @@ import { getCleanPromptName } from '../config/promptFileLocations.js';
|
||||
import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js';
|
||||
import { PromptFilesLocator } from '../utils/promptFilesLocator.js';
|
||||
import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js';
|
||||
import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage } from './promptsService.js';
|
||||
import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IClaudeSkill, IUserPromptPath, PromptsStorage, ICustomAgentQueryOptions, IExternalCustomAgent, ExtensionAgentSourceType } from './promptsService.js';
|
||||
import { Delayer } from '../../../../../../base/common/async.js';
|
||||
import { Schemas } from '../../../../../../base/common/network.js';
|
||||
import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
|
||||
|
||||
/**
|
||||
* Provides prompt services.
|
||||
@@ -154,16 +154,107 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
const prompts = await Promise.all([
|
||||
this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))),
|
||||
this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))),
|
||||
this.getExtensionContributions(type)
|
||||
this.getExtensionPromptFiles(type, token),
|
||||
]);
|
||||
|
||||
return [...prompts.flat()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of CustomAgentsProvider instances. Extensions can register providers via the proposed API.
|
||||
*/
|
||||
private readonly customAgentsProviders: Array<{
|
||||
extension: IExtensionDescription;
|
||||
onDidChangeCustomAgents?: Event<void>;
|
||||
provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise<IExternalCustomAgent[] | undefined>;
|
||||
}> = [];
|
||||
|
||||
/**
|
||||
* Registers a CustomAgentsProvider. This will be called by the extension host bridge when
|
||||
* an extension registers a provider via vscode.chat.registerCustomAgentsProvider().
|
||||
*/
|
||||
public registerCustomAgentsProvider(extension: IExtensionDescription, provider: {
|
||||
onDidChangeCustomAgents?: Event<void>;
|
||||
provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise<IExternalCustomAgent[] | undefined>;
|
||||
}): IDisposable {
|
||||
const providerEntry = { extension, ...provider };
|
||||
this.customAgentsProviders.push(providerEntry);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Listen to provider change events to rerun computeListPromptFiles
|
||||
if (provider.onDidChangeCustomAgents) {
|
||||
disposables.add(provider.onDidChangeCustomAgents(() => {
|
||||
this.cachedFileLocations[PromptsType.agent] = undefined;
|
||||
this.cachedCustomAgents.refresh();
|
||||
}));
|
||||
}
|
||||
|
||||
// Invalidate agent cache when providers change
|
||||
this.cachedFileLocations[PromptsType.agent] = undefined;
|
||||
this.cachedCustomAgents.refresh();
|
||||
|
||||
disposables.add({
|
||||
dispose: () => {
|
||||
const index = this.customAgentsProviders.findIndex((p) => p === providerEntry);
|
||||
if (index >= 0) {
|
||||
this.customAgentsProviders.splice(index, 1);
|
||||
this.cachedFileLocations[PromptsType.agent] = undefined;
|
||||
this.cachedCustomAgents.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private async listCustomAgentsFromProvider(token: CancellationToken): Promise<IPromptPath[]> {
|
||||
const result: IPromptPath[] = [];
|
||||
|
||||
if (this.customAgentsProviders.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Collect agents from all providers
|
||||
for (const providerEntry of this.customAgentsProviders) {
|
||||
try {
|
||||
const agents = await providerEntry.provideCustomAgents({}, token);
|
||||
if (!agents || token.isCancellationRequested) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const agent of agents) {
|
||||
try {
|
||||
await this.filesConfigService.updateReadonly(agent.uri, true);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
this.logger.error(`[listCustomAgentsFromProvider] Failed to make agent file readonly: ${agent.uri}`, msg);
|
||||
}
|
||||
|
||||
result.push({
|
||||
uri: agent.uri,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
storage: PromptsStorage.extension,
|
||||
type: PromptsType.agent,
|
||||
extension: providerEntry.extension,
|
||||
source: ExtensionAgentSourceType.provider
|
||||
} satisfies IExtensionPromptPath);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[listCustomAgentsFromProvider] Failed to get custom agents from provider`, e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise<readonly IPromptPath[]> {
|
||||
switch (storage) {
|
||||
case PromptsStorage.extension:
|
||||
return this.getExtensionContributions(type);
|
||||
return this.getExtensionPromptFiles(type, token);
|
||||
case PromptsStorage.local:
|
||||
return this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath)));
|
||||
case PromptsStorage.user:
|
||||
@@ -173,9 +264,14 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getExtensionContributions(type: PromptsType): Promise<IPromptPath[]> {
|
||||
private async getExtensionPromptFiles(type: PromptsType, token: CancellationToken): Promise<IPromptPath[]> {
|
||||
await this.extensionService.whenInstalledExtensionsRegistered();
|
||||
return Promise.all(this.contributedFiles[type].values());
|
||||
const contributedFiles = await Promise.all(this.contributedFiles[type].values());
|
||||
if (type === PromptsType.agent) {
|
||||
const providerAgents = await this.listCustomAgentsFromProvider(token);
|
||||
return [...contributedFiles, ...providerAgents];
|
||||
}
|
||||
return contributedFiles;
|
||||
}
|
||||
|
||||
public getSourceFolders(type: PromptsType): readonly IPromptPath[] {
|
||||
@@ -359,7 +455,7 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
this.logger.error(`[registerContributedFile] Failed to make prompt file readonly: ${uri}`, msg);
|
||||
}
|
||||
return { uri, name, description, storage: PromptsStorage.extension, type, extension } satisfies IExtensionPromptPath;
|
||||
return { uri, name, description, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath;
|
||||
})();
|
||||
bucket.set(uri, entryPromise);
|
||||
|
||||
@@ -578,7 +674,8 @@ namespace IAgentSource {
|
||||
if (promptPath.storage === PromptsStorage.extension) {
|
||||
return {
|
||||
storage: PromptsStorage.extension,
|
||||
extensionId: promptPath.extension.identifier
|
||||
extensionId: promptPath.extension.identifier,
|
||||
type: promptPath.source
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ITextModel } from '../../../../../editor/common/model.js';
|
||||
import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { ParsedPromptFile } from '../../common/promptSyntax/promptFileParser.js';
|
||||
import { IClaudeSkill, ICustomAgent, IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { IClaudeSkill, ICustomAgent, ICustomAgentQueryOptions, IExternalCustomAgent, IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { ResourceSet } from '../../../../../base/common/map.js';
|
||||
|
||||
export class MockPromptsService implements IPromptsService {
|
||||
@@ -53,6 +53,7 @@ export class MockPromptsService implements IPromptsService {
|
||||
getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); }
|
||||
getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); }
|
||||
setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); }
|
||||
registerCustomAgentsProvider(extension: IExtensionDescription, provider: { provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise<IExternalCustomAgent[] | undefined> }): IDisposable { throw new Error('Method not implemented.'); }
|
||||
findClaudeSkills(token: CancellationToken): Promise<IClaudeSkill[] | undefined> { throw new Error('Method not implemented.'); }
|
||||
dispose(): void { }
|
||||
}
|
||||
|
||||
+53
-1
@@ -36,7 +36,7 @@ import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '..
|
||||
import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js';
|
||||
import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js';
|
||||
import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js';
|
||||
import { ExtensionAgentSourceType, ICustomAgent, ICustomAgentQueryOptions, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js';
|
||||
import { mockFiles } from '../testUtils/mockFilesystem.js';
|
||||
import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js';
|
||||
@@ -1071,6 +1071,58 @@ suite('PromptsService', () => {
|
||||
assert.strictEqual(actual[0].type, PromptsType.instructions);
|
||||
registered.dispose();
|
||||
});
|
||||
|
||||
test('Custom agent provider', async () => {
|
||||
const agentUri = URI.parse('file://extensions/my-extension/myAgent.agent.md');
|
||||
const extension = {
|
||||
identifier: { value: 'test.my-extension' },
|
||||
enabledApiProposals: ['chatParticipantPrivate']
|
||||
} as unknown as IExtensionDescription;
|
||||
|
||||
// Mock the agent file content
|
||||
await mockFiles(fileService, [
|
||||
{
|
||||
path: agentUri.path,
|
||||
contents: [
|
||||
'---',
|
||||
'description: \'My custom agent from provider\'',
|
||||
'tools: [ tool1, tool2 ]',
|
||||
'---',
|
||||
'I am a custom agent from a provider.',
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const provider = {
|
||||
provideCustomAgents: async (_options: ICustomAgentQueryOptions, _token: CancellationToken) => {
|
||||
return [
|
||||
{
|
||||
name: 'myAgent',
|
||||
description: 'My custom agent from provider',
|
||||
uri: agentUri
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const registered = service.registerCustomAgentsProvider(extension, provider);
|
||||
|
||||
const actual = await service.getCustomAgents(CancellationToken.None);
|
||||
assert.strictEqual(actual.length, 1);
|
||||
assert.strictEqual(actual[0].name, 'myAgent');
|
||||
assert.strictEqual(actual[0].description, 'My custom agent from provider');
|
||||
assert.strictEqual(actual[0].uri.toString(), agentUri.toString());
|
||||
assert.strictEqual(actual[0].source.storage, PromptsStorage.extension);
|
||||
if (actual[0].source.storage === PromptsStorage.extension) {
|
||||
assert.strictEqual(actual[0].source.type, ExtensionAgentSourceType.provider);
|
||||
}
|
||||
|
||||
registered.dispose();
|
||||
|
||||
// After disposal, the agent should no longer be listed
|
||||
const actualAfterDispose = await service.getCustomAgents(CancellationToken.None);
|
||||
assert.strictEqual(actualAfterDispose.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('findClaudeSkills', () => {
|
||||
|
||||
@@ -314,4 +314,73 @@ declare module 'vscode' {
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region CustomAgentsProvider
|
||||
|
||||
/**
|
||||
* Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository.
|
||||
*/
|
||||
export interface CustomAgentResource {
|
||||
/**
|
||||
* The unique identifier/name of the custom agent resource.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* A description of what the custom agent resource does.
|
||||
*/
|
||||
readonly description: string;
|
||||
|
||||
/**
|
||||
* The URI to the agent or prompt resource file.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target environment for custom agents.
|
||||
*/
|
||||
export enum CustomAgentTarget {
|
||||
GitHubCopilot = 'github-copilot',
|
||||
VSCode = 'vscode',
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for querying custom agents.
|
||||
*/
|
||||
export interface CustomAgentQueryOptions {
|
||||
/**
|
||||
* Filter agents by target environment.
|
||||
*/
|
||||
readonly target?: CustomAgentTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories.
|
||||
*/
|
||||
export interface CustomAgentsProvider {
|
||||
/**
|
||||
* An optional event to signal that custom agents have changed.
|
||||
*/
|
||||
onDidChangeCustomAgents?: Event<void>;
|
||||
|
||||
/**
|
||||
* Provide the list of custom agent resources available for a given repository.
|
||||
* @param options Optional query parameters.
|
||||
* @param token A cancellation token.
|
||||
* @returns An array of custom agent resources or a promise that resolves to such.
|
||||
*/
|
||||
provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult<CustomAgentResource[]>;
|
||||
}
|
||||
|
||||
export namespace chat {
|
||||
/**
|
||||
* Register a provider for custom agents.
|
||||
* @param provider The custom agents provider.
|
||||
* @returns A disposable that unregisters the provider when disposed.
|
||||
*/
|
||||
export function registerCustomAgentsProvider(provider: CustomAgentsProvider): Disposable;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user