list agents to be used as subagents (#278624)

* instructions list: use XML and tool variables

* update

* update

* Merge branch 'main' into aeschli/xenial-felidae-488

* allow to run agents as subagents (#278971)

* fix

* Merge remote-tracking branch 'origin/main' into aeschli/xenial-felidae-488

* fix

* fix
This commit is contained in:
Martin Aeschlimann
2025-11-24 13:09:27 +01:00
committed by GitHub
parent 5f169bfa1b
commit 21ca4eb7b3
14 changed files with 318 additions and 89 deletions

View File

@@ -69,7 +69,7 @@ import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariable
import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js';
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js';
import { ILanguageModelToolsService, IToolData, ToolSet } from '../common/languageModelToolsService.js';
import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js';
import { ComputeAutomaticInstructions } from '../common/promptSyntax/computeAutomaticInstructions.js';
import { PromptsConfig } from '../common/promptSyntax/config/config.js';
import { IHandOff, PromptHeader, Target } from '../common/promptSyntax/promptFileParser.js';
@@ -2490,22 +2490,12 @@ export class ChatWidget extends Disposable implements IChatWidget {
*/
private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {
this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`);
const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.entriesMap.get() : undefined;
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this._getReadTool());
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools);
await computer.collect(attachedContext, CancellationToken.None);
}
private _getReadTool(): IToolData | undefined {
if (this.input.currentModeKind !== ChatModeKind.Agent) {
return undefined;
}
const readFileTool = this.toolsService.getToolByName('readFile');
if (!readFileTool || !this.input.selectedToolsModel.userSelectedTools.get()[readFileTool.id]) {
return undefined;
}
return readFileTool;
}
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {
this.tree.delegateScrollFromMouseWheelEvent(browserEvent);
}

View File

@@ -119,6 +119,7 @@ export class ChatModeService extends Disposable implements IChatModeService {
agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },
handOffs: cachedMode.handOffs,
target: cachedMode.target,
infer: cachedMode.infer,
source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local }
};
const instance = new CustomChatMode(customChatMode);
@@ -240,6 +241,7 @@ export interface IChatModeData {
readonly uri?: URI;
readonly source?: IChatModeSourceData;
readonly target?: string;
readonly infer?: boolean;
}
export interface IChatMode {
@@ -257,6 +259,7 @@ export interface IChatMode {
readonly uri?: IObservable<URI>;
readonly source?: IAgentSource;
readonly target?: IObservable<string | undefined>;
readonly infer?: IObservable<boolean | undefined>;
}
export interface IVariableReference {
@@ -287,7 +290,8 @@ function isCachedChatModeData(data: unknown): data is IChatModeData {
(mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&
(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&
(mode.source === undefined || isChatModeSourceData(mode.source)) &&
(mode.target === undefined || typeof mode.target === 'string');
(mode.target === undefined || typeof mode.target === 'string') &&
(mode.infer === undefined || typeof mode.infer === 'boolean');
}
export class CustomChatMode implements IChatMode {
@@ -300,6 +304,7 @@ export class CustomChatMode implements IChatMode {
private readonly _argumentHintObservable: ISettableObservable<string | undefined>;
private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;
private readonly _targetObservable: ISettableObservable<string | undefined>;
private readonly _inferObservable: ISettableObservable<boolean | undefined>;
private _source: IAgentSource;
public readonly id: string;
@@ -352,6 +357,10 @@ export class CustomChatMode implements IChatMode {
return this._targetObservable;
}
get infer(): IObservable<boolean | undefined> {
return this._inferObservable;
}
public readonly kind = ChatModeKind.Agent;
constructor(
@@ -365,6 +374,7 @@ export class CustomChatMode implements IChatMode {
this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint);
this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs);
this._targetObservable = observableValue('target', customChatMode.target);
this._inferObservable = observableValue('infer', customChatMode.infer);
this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions);
this._uriObservable = observableValue('uri', customChatMode.uri);
this._source = customChatMode.source;
@@ -382,6 +392,7 @@ export class CustomChatMode implements IChatMode {
this._argumentHintObservable.set(newData.argumentHint, tx);
this._handoffsObservable.set(newData.handOffs, tx);
this._targetObservable.set(newData.target, tx);
this._inferObservable.set(newData.infer, tx);
this._modeInstructions.set(newData.agentInstructions, tx);
this._uriObservable.set(newData.uri, tx);
this._source = newData.source;
@@ -401,7 +412,8 @@ export class CustomChatMode implements IChatMode {
uri: this.uri.get(),
handOffs: this.handOffs.get(),
source: serializeChatModeSource(this._source),
target: this.target.get()
target: this.target.get(),
infer: this.infer.get()
};
}
}

View File

@@ -16,13 +16,15 @@ import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind } from '../chatVariableEntries.js';
import { IToolData } from '../languageModelToolsService.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../chatVariableEntries.js';
import { ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, VSCodeToolReference } from '../languageModelToolsService.js';
import { PromptsConfig } from './config/config.js';
import { isPromptOrInstructionsFile } from './config/promptFileLocations.js';
import { PromptsType } from './promptTypes.js';
import { ParsedPromptFile } from './promptFileParser.js';
import { IPromptPath, IPromptsService } from './service/promptsService.js';
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
import { ChatConfiguration } from '../constants.js';
export type InstructionsCollectionEvent = {
applyingInstructionsCount: number;
@@ -50,7 +52,7 @@ export class ComputeAutomaticInstructions {
private _parseResults: ResourceMap<ParsedPromptFile> = new ResourceMap();
constructor(
private readonly _readFileTool: IToolData | undefined,
private readonly _enabledTools: IToolAndToolSetEnablementMap | undefined,
@IPromptsService private readonly _promptsService: IPromptsService,
@ILogService public readonly _logService: ILogService,
@ILabelService private readonly _labelService: ILabelService,
@@ -58,6 +60,7 @@ export class ComputeAutomaticInstructions {
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IFileService private readonly _fileService: IFileService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService,
) {
}
@@ -94,10 +97,9 @@ export class ComputeAutomaticInstructions {
// get copilot instructions
await this._addAgentInstructions(variables, telemetryEvent, token);
const instructionsWithPatternsList = await this._getInstructionsWithPatternsList(instructionFiles, variables, token);
if (instructionsWithPatternsList.length > 0) {
const text = instructionsWithPatternsList.join('\n');
variables.add(toPromptTextVariableEntry(text, true));
const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, token);
if (instructionsListVariable) {
variables.add(instructionsListVariable);
telemetryEvent.listedInstructionsCount++;
}
@@ -234,65 +236,136 @@ export class ComputeAutomaticInstructions {
return undefined;
}
private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise<string[]> {
if (!this._readFileTool) {
this._logService.trace('[InstructionsContextComputer] No readFile tool available, skipping instructions with patterns list.');
return [];
private _getTool(referenceName: string): { tool: IToolData; variable: string } | undefined {
if (!this._enabledTools) {
return undefined;
}
const searchNestedAgentMd = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD);
const agentsMdPromise = searchNestedAgentMd ? this._promptsService.findAgentMDsInWorkspace(token) : Promise.resolve([]);
const tool = this._languageModelToolsService.getToolByName(referenceName);
if (tool && this._enabledTools.get(tool)) {
return { tool, variable: `#tool:${this._languageModelToolsService.getFullReferenceName(tool)}` };
}
return undefined;
}
const toolName = 'read_file'; // workaround https://github.com/microsoft/vscode/issues/252167
const entries: string[] = [
'Here is a list of instruction files that contain rules for modifying or creating new code.',
'These files are important for ensuring that the code is modified or created correctly.',
'Please make sure to follow the rules specified in these files when working with the codebase.',
`If the file is not already available as attachment, use the \`${toolName}\` tool to acquire it.`,
'Make sure to acquire the instructions before making any changes to the code.',
'| File | Applies To | Description |',
'| ------- | --------- | ----------- |',
];
let hasContent = false;
for (const { uri } of instructionFiles) {
const parsedFile = await this._parseInstructionsFile(uri, token);
if (parsedFile) {
const applyTo = parsedFile.header?.applyTo ?? '';
const description = parsedFile.header?.description ?? '';
entries.push(`| '${getFilePath(uri)}' | ${applyTo} | ${description} |`);
hasContent = true;
private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise<IPromptTextVariableEntry | undefined> {
const readTool = this._getTool('readFile');
const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent);
const entries: string[] = [];
if (readTool) {
const searchNestedAgentMd = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD);
const agentsMdPromise = searchNestedAgentMd ? this._promptsService.findAgentMDsInWorkspace(token) : Promise.resolve([]);
entries.push('<instructions>');
entries.push('Here is a list of instruction files that contain rules for modifying or creating new code.');
entries.push('These files are important for ensuring that the code is modified or created correctly.');
entries.push('Please make sure to follow the rules specified in these files when working with the codebase.');
entries.push(`If the file is not already available as attachment, use the ${readTool.variable} tool to acquire it.`);
entries.push('Make sure to acquire the instructions before making any changes to the code.');
let hasContent = false;
for (const { uri } of instructionFiles) {
const parsedFile = await this._parseInstructionsFile(uri, token);
if (parsedFile) {
entries.push('<instruction>');
if (parsedFile.header) {
const { description, applyTo } = parsedFile.header;
if (description) {
entries.push(`<description>${description}</description>`);
}
entries.push(`<file>${getFilePath(uri)}</file>`);
if (applyTo) {
entries.push(`<applyTo>${applyTo}</applyTo>`);
}
}
entries.push('</instruction>');
hasContent = true;
}
}
const agentsMdFiles = await agentsMdPromise;
for (const uri of agentsMdFiles) {
if (uri) {
const folderName = this._labelService.getUriLabel(dirname(uri), { relative: true });
const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName);
entries.push('<instruction>');
entries.push(`<description>${description}</description>`);
entries.push(`<file>${getFilePath(uri)}</file>`);
entries.push('</instruction>');
hasContent = true;
}
}
if (!hasContent) {
entries.length = 0; // clear entries
} else {
entries.push('</instructions>', '', ''); // add trailing newline
}
const claudeSkills = await this._promptsService.findClaudeSkills(token);
if (claudeSkills && claudeSkills.length > 0) {
entries.push('<skills>');
entries.push('Here is a list of skills that contain domain specific knowledge on a variety of topics.');
entries.push('Each skill comes with a description of the topic and a file path that contains the detailed instructions.');
entries.push(`When a user asks you to perform a task that falls within the domain of a skill, use the ${readTool.variable} tool to acquire the full instructions from the file URI.`);
for (const skill of claudeSkills) {
entries.push('<skill>');
entries.push(`<name>${skill.name}</name>`);
if (skill.description) {
entries.push(`<description>${skill.description}</description>`);
}
entries.push(`<file>${getFilePath(skill.uri)}</file>`);
entries.push('</skill>');
}
entries.push('</skills>', '', ''); // add trailing newline
}
}
const agentsMdFiles = await agentsMdPromise;
for (const uri of agentsMdFiles) {
if (uri) {
const folderName = this._labelService.getUriLabel(dirname(uri), { relative: true });
const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName);
entries.push(`| '${getFilePath(uri)}' | | ${description} |`);
hasContent = true;
if (runSubagentTool) {
const subagentToolCustomAgents = this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents);
if (subagentToolCustomAgents) {
const agents = await this._promptsService.getCustomAgents(token);
if (agents.length > 0) {
entries.push('<agents>');
entries.push('Here is a list of agents that can be used when running a subagent.');
entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.');
entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`);
for (const agent of agents) {
if (agent.infer === false) {
// skip agents that are not meant for subagent use
continue;
}
entries.push('<agent>');
entries.push(`<name>${agent.name}</name>`);
if (agent.description) {
entries.push(`<description>${agent.description}</description>`);
}
if (agent.argumentHint) {
entries.push(`<argumentHint>${agent.argumentHint}</argumentHint>`);
}
entries.push('</agent>');
}
entries.push('</agents>', '', ''); // add trailing newline
}
}
}
if (!hasContent) {
entries.length = 0; // clear entries
} else {
entries.push('', ''); // add trailing newline
if (entries.length === 0) {
return undefined;
}
const claudeSkills = await this._promptsService.findClaudeSkills(token);
if (claudeSkills && claudeSkills.length > 0) {
entries.push(
'Here is a list of skills that contain domain specific knowledge on a variety of topics.',
'Each skill comes with a description of the topic and a file path that contains the detailed instructions.',
'When a user asks you to perform a task that falls within the domain of a skill, use the \`${toolName}\` tool to acquire the full instructions from the file URI.',
'| Name | Description | File',
'| ------- | --------- | ----------- |',
);
for (const skill of claudeSkills) {
entries.push(`| ${skill.name} | ${skill.description} | '${getFilePath(skill.uri)}' |`);
const content = entries.join('\n');
const toolReferences: ChatRequestToolReferenceEntry[] = [];
const collectToolReference = (tool: { tool: IToolData; variable: string } | undefined) => {
if (tool) {
let offset = content.indexOf(tool.variable);
while (offset >= 0) {
toolReferences.push(toToolVariableEntry(tool.tool, new OffsetRange(offset, offset + tool.variable.length)));
offset = content.indexOf(tool.variable, offset + 1);
}
}
}
return entries;
};
collectToolReference(readTool);
collectToolReference(runSubagentTool);
return toPromptTextVariableEntry(content, true, toolReferences);
}
private async _addReferencedInstructions(attachedContext: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise<void> {

View File

@@ -199,6 +199,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
}
return suggestions;
}
break;
case PromptHeaderAttributes.target:
if (promptType === PromptsType.agent) {
return ['vscode', 'github-copilot'];
@@ -213,6 +214,12 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
if (promptType === PromptsType.prompt || promptType === PromptsType.agent) {
return this.getModelNames(promptType === PromptsType.agent);
}
break;
case PromptHeaderAttributes.infer:
if (promptType === PromptsType.agent) {
return ['true', 'false'];
}
break;
}
return [];
}

View File

@@ -100,6 +100,8 @@ export class PromptHoverProvider implements HoverProvider {
return this.getHandsOffHover(attribute, position, isGitHubTarget);
case PromptHeaderAttributes.target:
return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range);
case PromptHeaderAttributes.infer:
return this.createHover(localize('promptHeader.agent.infer', 'Whether the agent can be used as a subagent.'), attribute.range);
}
}
}

View File

@@ -150,6 +150,7 @@ export class PromptValidator {
case PromptsType.agent: {
this.validateTarget(attributes, report);
this.validateInfer(attributes, report);
this.validateTools(attributes, ChatModeKind.Agent, header.target, report);
if (!isGitHubTarget) {
this.validateModel(attributes, ChatModeKind.Agent, report);
@@ -463,6 +464,17 @@ export class PromptValidator {
}
}
private validateInfer(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined {
const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.infer);
if (!attribute) {
return;
}
if (attribute.value.type !== 'boolean') {
report(toMarker(localize('promptValidator.inferMustBeBoolean', "The 'infer' attribute must be a boolean."), attribute.value.range, MarkerSeverity.Error));
return;
}
}
private validateTarget(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined {
const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.target);
if (!attribute) {
@@ -487,7 +499,7 @@ export class PromptValidator {
const allAttributeNames = {
[PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint],
[PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent],
[PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target]
[PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer]
};
const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers];
const recommendedAttributeNames = {

View File

@@ -74,6 +74,7 @@ export namespace PromptHeaderAttributes {
export const argumentHint = 'argument-hint';
export const excludeAgent = 'excludeAgent';
export const target = 'target';
export const infer = 'infer';
}
export namespace GithubPromptHeaderAttributes {
@@ -159,6 +160,14 @@ export class PromptHeader {
return undefined;
}
private getBooleanAttribute(key: string): boolean | undefined {
const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
if (attribute?.value.type === 'boolean') {
return attribute.value.value;
}
return undefined;
}
public get name(): string | undefined {
return this.getStringAttribute(PromptHeaderAttributes.name);
}
@@ -187,6 +196,10 @@ export class PromptHeader {
return this.getStringAttribute(PromptHeaderAttributes.target);
}
public get infer(): boolean | undefined {
return this.getBooleanAttribute(PromptHeaderAttributes.infer);
}
public get tools(): string[] | undefined {
const toolsAttribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.tools);
if (!toolsAttribute) {

View File

@@ -118,6 +118,11 @@ export interface ICustomAgent {
*/
readonly target?: string;
/**
* Infer metadata in the prompt header.
*/
readonly infer?: boolean;
/**
* Contents of the custom agent file body and other agent instructions.
*/

View File

@@ -323,8 +323,8 @@ export class PromptsService extends Disposable implements IPromptsService {
if (!ast.header) {
return { uri, name, agentInstructions, source };
}
const { description, model, tools, handOffs, argumentHint, target } = ast.header;
return { uri, name, description, model, tools, handOffs, argumentHint, target, agentInstructions, source };
const { description, model, tools, handOffs, argumentHint, target, infer } = ast.header;
return { uri, name, description, model, tools, handOffs, argumentHint, target, infer, agentInstructions, source };
})
);
return customAgents;

View File

@@ -5,12 +5,13 @@
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Event } from '../../../../../base/common/event.js';
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
import { IJSONSchema, IJSONSchemaMap } from '../../../../../base/common/jsonSchema.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IChatAgentRequest, IChatAgentService } from '../chatAgents.js';
import { ChatModel, IChatRequestModeInstructions } from '../chatModel.js';
@@ -48,11 +49,13 @@ const BaseModelDescription = `Launch a new agent to handle complex, multi-step t
interface IRunSubagentToolInputParams {
prompt: string;
description: string;
subagentType?: string;
agentName?: string;
}
export class RunSubagentTool extends Disposable implements IToolImpl {
readonly onDidUpdateToolData: Event<IConfigurationChangeEvent>;
constructor(
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IChatService private readonly chatService: IChatService,
@@ -64,6 +67,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents));
}
getToolData(): IToolData {
@@ -84,11 +88,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
};
if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) {
inputSchema.properties.subagentType = {
inputSchema.properties.agentName = {
type: 'string',
description: 'Optional ID of a specific agent to invoke. If not provided, uses the current agent.'
description: 'Optional name of a specific agent to invoke. If not provided, uses the current agent.'
};
modelDescription += `\n- If the user asks for a certain agent by name, you MUST provide that EXACT subagentType (case-sensitive) to invoke that specific agent.`;
modelDescription += `\n- If the user asks for a certain agent, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`;
}
const runSubagentToolData: IToolData = {
id: RunSubagentToolId,
@@ -133,8 +137,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
let modeTools = invocation.userSelectedTools;
let modeInstructions: IChatRequestModeInstructions | undefined;
if (args.subagentType) {
const mode = this.chatModeService.findModeByName(args.subagentType);
if (args.agentName) {
const mode = this.chatModeService.findModeByName(args.agentName);
if (mode) {
// Use mode-specific model if available
const modeModelQualifiedName = mode.model?.get();
@@ -172,7 +176,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
metadata: instructions.metadata,
};
} else {
this.logService.warn(`RunSubagentTool: Agent '${args.subagentType}' not found, using current configuration`);
this.logService.warn(`RunSubagentTool: Agent '${args.agentName}' not found, using current configuration`);
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../base/common/codicons.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
@@ -42,14 +42,30 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo
this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool));
const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool));
const runSubagentToolData = runSubagentTool.getToolData();
this._register(toolsService.registerTool(runSubagentToolData, runSubagentTool));
const customAgentToolSet = this._register(toolsService.createToolSet(ToolDataSource.Internal, 'custom-agent', VSCodeToolReference.agent, {
icon: ThemeIcon.fromId(Codicon.agent.id),
description: localize('toolset.custom-agent', 'Delegate tasks to other agents'),
}));
this._register(customAgentToolSet.addTool(runSubagentToolData));
let runSubagentRegistration: IDisposable | undefined;
let toolSetRegistration: IDisposable | undefined;
const registerRunSubagentTool = () => {
runSubagentRegistration?.dispose();
toolSetRegistration?.dispose();
const runSubagentToolData = runSubagentTool.getToolData();
runSubagentRegistration = toolsService.registerTool(runSubagentToolData, runSubagentTool);
toolSetRegistration = customAgentToolSet.addTool(runSubagentToolData);
};
registerRunSubagentTool();
this._register(runSubagentTool.onDidUpdateToolData(registerRunSubagentTool));
this._register({
dispose: () => {
runSubagentRegistration?.dispose();
toolSetRegistration?.dispose();
}
});
}
}

View File

@@ -273,6 +273,18 @@ suite('PromptHoverProvider', () => {
const hover = await getHover(content, 2, 1, PromptsType.agent);
assert.strictEqual(hover, 'The name of the agent as shown in the UI.');
});
test('hover on infer attribute shows description', async () => {
const content = [
'---',
'name: "Test Agent"',
'description: "Test agent"',
'infer: true',
'---',
].join('\n');
const hover = await getHover(content, 4, 1, PromptsType.agent);
assert.strictEqual(hover, 'Whether the agent can be used as a subagent.');
});
});
suite('prompt hovers', () => {

View File

@@ -400,7 +400,7 @@ suite('PromptValidator', () => {
assert.deepStrictEqual(
markers.map(m => ({ severity: m.severity, message: m.message })),
[
{ severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: argument-hint, description, handoffs, model, name, target, tools.` },
{ severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: argument-hint, description, handoffs, infer, model, name, target, tools.` },
]
);
});
@@ -733,6 +733,81 @@ suite('PromptValidator', () => {
assert.deepStrictEqual(markers, [], 'Name should be optional for vscode target');
}
});
test('infer attribute validation', async () => {
// Valid infer: true
{
const content = [
'---',
'name: "TestAgent"',
'description: "Test agent"',
'infer: true',
'---',
'Body',
].join('\n');
const markers = await validate(content, PromptsType.agent);
assert.deepStrictEqual(markers, [], 'Valid infer: true should not produce errors');
}
// Valid infer: false
{
const content = [
'---',
'name: "TestAgent"',
'description: "Test agent"',
'infer: false',
'---',
'Body',
].join('\n');
const markers = await validate(content, PromptsType.agent);
assert.deepStrictEqual(markers, [], 'Valid infer: false should not produce errors');
}
// Invalid infer: string value
{
const content = [
'---',
'name: "TestAgent"',
'description: "Test agent"',
'infer: "yes"',
'---',
'Body',
].join('\n');
const markers = await validate(content, PromptsType.agent);
assert.strictEqual(markers.length, 1);
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
assert.strictEqual(markers[0].message, `The 'infer' attribute must be a boolean.`);
}
// Invalid infer: number value
{
const content = [
'---',
'name: "TestAgent"',
'description: "Test agent"',
'infer: 1',
'---',
'Body',
].join('\n');
const markers = await validate(content, PromptsType.agent);
assert.strictEqual(markers.length, 1);
assert.strictEqual(markers[0].severity, MarkerSeverity.Error);
assert.strictEqual(markers[0].message, `The 'infer' attribute must be a boolean.`);
}
// Missing infer attribute (should be optional)
{
const content = [
'---',
'name: "TestAgent"',
'description: "Test agent"',
'---',
'Body',
].join('\n');
const markers = await validate(content, PromptsType.agent);
assert.deepStrictEqual(markers, [], 'Missing infer attribute should be allowed');
}
});
});
suite('instructions', () => {

View File

@@ -724,6 +724,7 @@ suite('PromptsService', () => {
argumentHint: undefined,
tools: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -778,6 +779,7 @@ suite('PromptsService', () => {
model: undefined,
argumentHint: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
source: { storage: PromptsStorage.local },
},
@@ -849,6 +851,7 @@ suite('PromptsService', () => {
handOffs: undefined,
model: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -865,6 +868,7 @@ suite('PromptsService', () => {
model: undefined,
tools: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -933,6 +937,7 @@ suite('PromptsService', () => {
handOffs: undefined,
model: undefined,
argumentHint: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -949,6 +954,7 @@ suite('PromptsService', () => {
handOffs: undefined,
argumentHint: undefined,
tools: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -965,6 +971,7 @@ suite('PromptsService', () => {
argumentHint: undefined,
tools: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'),
source: { storage: PromptsStorage.local }
},
@@ -1018,6 +1025,7 @@ suite('PromptsService', () => {
model: undefined,
argumentHint: undefined,
target: undefined,
infer: undefined,
uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'),
source: { storage: PromptsStorage.local },
},