mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-20 17:59:17 +00:00
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:
committed by
GitHub
parent
5f169bfa1b
commit
21ca4eb7b3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user