Add support for custom chat agents in the API (#298227)

* Add support for custom chat agents in the API

- Introduced `chatCustomAgents` proposal in extensions API.
- Implemented methods to handle custom agents in `MainThreadChatAgents2`.
- Added `ICustomAgentDto` interface and related functionality in extHost.
- Created new type definitions for custom agents in `vscode.proposed.chatCustomAgents.d.ts`.

* Filter custom agents by visibility before pushing to the proxy

* Refactor onDidChangeCustomAgents to use direct event listener

* Update custom agent tools property to allow undefined values

* Add chatCustomAgents to enabledApiProposals in package.json

* update

* update

* support skills

* support instructions

* update

* update

---------

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
This commit is contained in:
Don Jayamanne
2026-03-05 00:38:47 +11:00
committed by GitHub
parent c5e1a4bdea
commit 284bd98ce3
9 changed files with 187 additions and 5 deletions

View File

@@ -9,6 +9,7 @@
"authSession",
"environmentPower",
"chatParticipantPrivate",
"chatPromptFiles",
"chatProvider",
"contribStatusBarItems",
"contribViewsRemote",

View File

@@ -40,7 +40,7 @@ import { ILanguageModelToolsService } from '../../contrib/chat/common/tools/lang
import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js';
import { IExtensionService } from '../../services/extensions/common/extensions.js';
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js';
import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IInstructionDto, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js';
import { NotebookDto } from './mainThreadNotebookDto.js';
interface AgentData {
@@ -153,6 +153,24 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
// Push the initial active session if there is already a focused widget
this._acceptActiveChatSession(this._chatWidgetService.lastFocusedWidget);
// Push custom agents to ext host
void this._pushCustomAgents();
this._register(this._promptsService.onDidChangeCustomAgents(() => {
void this._pushCustomAgents();
}));
// Push instructions to ext host
void this._pushInstructions();
this._register(this._promptsService.onDidChangeInstructions(() => {
void this._pushInstructions();
}));
// Push skills to ext host
void this._pushSkills();
this._register(this._promptsService.onDidChangeSkills(() => {
void this._pushSkills();
}));
}
private _acceptActiveChatSession(widget: IChatWidget | undefined): void {
@@ -161,6 +179,36 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._proxy.$acceptActiveChatSession(isLocal ? sessionResource : undefined);
}
private async _pushCustomAgents(): Promise<void> {
try {
const customAgents = await this._promptsService.getCustomAgents(CancellationToken.None);
const dtos: ICustomAgentDto[] = customAgents.map(agent => ({ uri: agent.uri }));
this._proxy.$acceptCustomAgents(dtos);
} catch (error) {
this._logService.error('[chat] Failed to push custom agents to extension host', error);
}
}
private async _pushInstructions(): Promise<void> {
try {
const instructions = await this._promptsService.getInstructionFiles(CancellationToken.None);
const dtos: IInstructionDto[] = instructions.map(instruction => ({ uri: instruction.uri }));
this._proxy.$acceptInstructions(dtos);
} catch (error) {
this._logService.error('[chat] Failed to push instructions to extension host', error);
}
}
private async _pushSkills(): Promise<void> {
try {
const skills = await this._promptsService.findAgentSkills(CancellationToken.None) ?? [];
const dtos: ISkillDto[] = skills.map(skill => ({ uri: skill.uri }));
this._proxy.$acceptSkills(dtos);
} catch (error) {
this._logService.error('[chat] Failed to push skills to extension host', error);
}
}
$unregisterAgent(handle: number): void {
this._agents.deleteAndDispose(handle);
}

View File

@@ -1677,6 +1677,30 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatDebug');
return extHostChatDebug.registerChatDebugLogProvider(provider);
},
get customAgents() {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.customAgents as readonly vscode.ChatResource[];
},
onDidChangeCustomAgents: (listener, thisArgs?, disposables?) => {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.onDidChangeCustomAgents(listener, thisArgs, disposables);
},
get instructions() {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.instructions as readonly vscode.ChatResource[];
},
onDidChangeInstructions: (listener, thisArgs?, disposables?) => {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.onDidChangeInstructions(listener, thisArgs, disposables);
},
get skills() {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.skills as readonly vscode.ChatResource[];
},
onDidChangeSkills: (listener, thisArgs?, disposables?) => {
checkProposedApiEnabled(extension, 'chatPromptFiles');
return extHostChatAgents2.onDidChangeSkills(listener, thisArgs, disposables);
},
};
// namespace: lm

View File

@@ -1614,6 +1614,21 @@ export interface ExtHostChatAgentsShape2 {
$setRequestTools(requestId: string, tools: UserSelectedTools): void;
$setYieldRequested(requestId: string, value: boolean): void;
$acceptActiveChatSession(sessionResource: UriComponents | undefined): void;
$acceptCustomAgents(agents: ICustomAgentDto[]): void;
$acceptInstructions(instructions: IInstructionDto[]): void;
$acceptSkills(skills: ISkillDto[]): void;
}
export interface ICustomAgentDto {
uri: UriComponents;
}
export interface IInstructionDto {
uri: UriComponents;
}
export interface ISkillDto {
uri: UriComponents;
}
export interface IChatParticipantMetadata {
participant: string;

View File

@@ -27,7 +27,7 @@ import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js';
import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IExtensionChatAgentMetadata, IInstructionDto, IMainContext, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js';
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
import { ExtHostDiagnostics } from './extHostDiagnostics.js';
import { ExtHostDocuments } from './extHostDocuments.js';
@@ -487,6 +487,17 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
private readonly _onDidDisposeChatSession = this._register(new Emitter<string>());
readonly onDidDisposeChatSession = this._onDidDisposeChatSession.event;
private readonly _onDidChangeCustomAgents = this._register(new Emitter<void>());
readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event;
private readonly _onDidChangeInstructions = this._register(new Emitter<void>());
readonly onDidChangeInstructions = this._onDidChangeInstructions.event;
private readonly _onDidChangeSkills = this._register(new Emitter<void>());
readonly onDidChangeSkills = this._onDidChangeSkills.event;
private _customAgents: vscode.ChatResource[] = [];
private _instructions: vscode.ChatResource[] = [];
private _skills: vscode.ChatResource[] = [];
private _activeChatPanelSessionResource: URI | undefined;
private readonly _onDidChangeActiveChatPanelSessionResource = this._register(new Emitter<URI | undefined>());
@@ -496,6 +507,33 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
return this._activeChatPanelSessionResource;
}
get customAgents(): readonly vscode.ChatResource[] {
return this._customAgents;
}
get instructions(): readonly vscode.ChatResource[] {
return this._instructions;
}
get skills(): readonly vscode.ChatResource[] {
return this._skills;
}
$acceptCustomAgents(agents: ICustomAgentDto[]): void {
this._customAgents = agents.map(a => Object.freeze({ uri: URI.revive(a.uri) }));
this._onDidChangeCustomAgents.fire();
}
$acceptInstructions(instructions: IInstructionDto[]): void {
this._instructions = instructions.map(i => Object.freeze({ uri: URI.revive(i.uri) }));
this._onDidChangeInstructions.fire();
}
$acceptSkills(skills: ISkillDto[]): void {
this._skills = skills.map(s => Object.freeze({ uri: URI.revive(s.uri) }));
this._onDidChangeSkills.fire();
}
constructor(
mainContext: IMainContext,
private readonly _logService: ILogService,

View File

@@ -417,6 +417,11 @@ export interface IPromptsService extends IDisposable {
*/
readonly onDidChangeCustomAgents: Event<void>;
/**
* Event that is triggered when the list of instruction files changes.
*/
readonly onDidChangeInstructions: Event<void>;
/**
* Finds all available custom agents
* @param sessionResource Optional session resource to scope debug logging to a specific session.
@@ -483,6 +488,11 @@ export interface IPromptsService extends IDisposable {
*/
findAgentSkills(token: CancellationToken, sessionResource?: URI): Promise<IAgentSkill[] | undefined>;
/**
* Event that is triggered when the list of skills changes.
*/
readonly onDidChangeSkills: Event<void>;
/**
* Gets detailed discovery information for a prompt type.
* This includes all files found and their load/skip status with reasons.

View File

@@ -152,6 +152,7 @@ export class PromptsService extends Disposable implements IPromptsService {
private readonly _contributedWhenKeys = new Set<string>();
private readonly _contributedWhenClauses = new Map<string, string>();
private readonly _onDidContributedWhenChange = this._register(new Emitter<void>());
private readonly _onDidChangeInstructions = this._register(new Emitter<void>());
private readonly _onDidPluginPromptFilesChange = this._register(new Emitter<void>());
private readonly _onDidPluginHooksChange = this._register(new Emitter<void>());
private _pluginPromptFilesByType = new Map<PromptsType, readonly IPluginPromptPath[]>();
@@ -417,6 +418,7 @@ export class PromptsService extends Disposable implements IPromptsService {
this.cachedCustomAgents.refresh();
} else if (type === PromptsType.instructions) {
this.cachedFileLocations[PromptsType.instructions] = undefined;
this._onDidChangeInstructions.fire();
} else if (type === PromptsType.prompt) {
this.cachedFileLocations[PromptsType.prompt] = undefined;
this.cachedSlashCommands.refresh();
@@ -644,6 +646,14 @@ export class PromptsService extends Disposable implements IPromptsService {
return this.cachedCustomAgents.onDidChange;
}
public get onDidChangeInstructions(): Event<void> {
return Event.any(
this.getFileLocatorEvent(PromptsType.instructions),
this._onDidContributedWhenChange.event,
this._onDidChangeInstructions.event,
);
}
public async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise<readonly ICustomAgent[]> {
const sw = StopWatch.create();
const result = await this.cachedCustomAgents.get(token);

View File

@@ -18,8 +18,8 @@ export class MockPromptsService implements IPromptsService {
_serviceBrand: undefined;
private readonly _onDidChangeCustomChatModes = new Emitter<void>();
readonly onDidChangeCustomAgents = this._onDidChangeCustomChatModes.event;
private readonly _onDidChangeCustomAgents = new Emitter<void>();
readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event;
private readonly _onDidLogDiscovery = new Emitter<IPromptDiscoveryLogEntry>();
readonly onDidLogDiscovery: Event<IPromptDiscoveryLogEntry> = this._onDidLogDiscovery.event;
@@ -28,7 +28,7 @@ export class MockPromptsService implements IPromptsService {
setCustomModes(modes: ICustomAgent[]): void {
this._customModes = modes;
this._onDidChangeCustomChatModes.fire();
this._onDidChangeCustomAgents.fire();
}
async getCustomAgents(token: CancellationToken, sessionResource?: URI): Promise<readonly ICustomAgent[]> {
@@ -72,4 +72,7 @@ export class MockPromptsService implements IPromptsService {
getHooks(_token: CancellationToken, _sessionResource?: URI): Promise<any> { throw new Error('Method not implemented.'); }
getInstructionFiles(_token: CancellationToken, _sessionResource?: URI): Promise<readonly IPromptPath[]> { throw new Error('Method not implemented.'); }
dispose(): void { }
onDidChangeInstructions: Event<void> = Event.None;
onDidChangePromptFiles: Event<void> = Event.None;
onDidChangeSkills: Event<void> = Event.None;
}

View File

@@ -103,6 +103,39 @@ declare module 'vscode' {
// #region Chat Provider Registration
export namespace chat {
/**
* An event that fires when the list of {@link customAgents custom agents} changes.
*/
export const onDidChangeCustomAgents: Event<void>;
/**
* The list of currently available custom agents. These are `.agent.md` files
* from all sources (workspace, user, and extension-provided).
*/
export const customAgents: readonly ChatResource[];
/**
* An event that fires when the list of {@link instructions instructions} changes.
*/
export const onDidChangeInstructions: Event<void>;
/**
* The list of currently available instructions. These are `.instructions.md` files
* from all sources (workspace, user, and extension-provided).
*/
export const instructions: readonly ChatResource[];
/**
* An event that fires when the list of {@link skills skills} changes.
*/
export const onDidChangeSkills: Event<void>;
/**
* The list of currently available skills. These are `SKILL.md` files
* from all sources (workspace, user, and extension-provided).
*/
export const skills: readonly ChatResource[];
/**
* Register a provider for custom agents.
* @param provider The custom agent provider.