diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 2d764f09db3..5521189bd64 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -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 { 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); } diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 0ed0770b581..620ea5e5985 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -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; readonly source?: IAgentSource; readonly target?: IObservable; + readonly infer?: IObservable; } 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; private readonly _handoffsObservable: ISettableObservable; private readonly _targetObservable: ISettableObservable; + private readonly _inferObservable: ISettableObservable; private _source: IAgentSource; public readonly id: string; @@ -352,6 +357,10 @@ export class CustomChatMode implements IChatMode { return this._targetObservable; } + get infer(): IObservable { + 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() }; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index b4000b13ddb..c8accbbf2f4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -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 = 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 { - 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 { + 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(''); + 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(''); + if (parsedFile.header) { + const { description, applyTo } = parsedFile.header; + if (description) { + entries.push(`${description}`); + } + entries.push(`${getFilePath(uri)}`); + if (applyTo) { + entries.push(`${applyTo}`); + } + } + entries.push(''); + 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(''); + entries.push(`${description}`); + entries.push(`${getFilePath(uri)}`); + entries.push(''); + hasContent = true; + } + } + + if (!hasContent) { + entries.length = 0; // clear entries + } else { + entries.push('', '', ''); // add trailing newline + } + + const claudeSkills = await this._promptsService.findClaudeSkills(token); + if (claudeSkills && claudeSkills.length > 0) { + entries.push(''); + 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(''); + entries.push(`${skill.name}`); + if (skill.description) { + entries.push(`${skill.description}`); + } + entries.push(`${getFilePath(skill.uri)}`); + entries.push(''); + } + entries.push('', '', ''); // 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(''); + 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(''); + entries.push(`${agent.name}`); + if (agent.description) { + entries.push(`${agent.description}`); + } + if (agent.argumentHint) { + entries.push(`${agent.argumentHint}`); + } + entries.push(''); + } + entries.push('', '', ''); // 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 { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 6150f59dd03..bfe7f275eed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -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 []; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 6a42558b4c2..ba708753fba 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -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); } } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 7eff65bdaad..68694e53758 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -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 = { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 76d9c58b0de..8ca00a9dff2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -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) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index aadb112753f..ebc7fa55d99 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -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. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index f8846e9d97e..df33a1041ec 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -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; diff --git a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts index 0b0f84fb82f..2a5d920b668 100644 --- a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts @@ -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; + 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`); } } diff --git a/src/vs/workbench/contrib/chat/common/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/tools.ts index b79a2b107b2..8a453350848 100644 --- a/src/vs/workbench/contrib/chat/common/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/tools.ts @@ -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(); + } + }); + + } } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts index da0ca64d664..e771702eb4c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts @@ -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', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts index 0f47bb59813..c672f6dc028 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts @@ -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', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 32819b8435c..bc4b39d9d81 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -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 }, },