diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 06e553b252893..690322fdd1170 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1359,6 +1359,15 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, + [ChatConfiguration.GeneralPurposeAgentEnabled]: { + type: 'boolean', + description: nls.localize('chat.generalPurposeAgent.enabled', "Controls whether the built-in General Purpose agent is available as a subagent."), + default: false, + tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.SubagentsAllowInvocationsFromSubagents]: { type: 'boolean', description: nls.localize('chat.subagents.allowInvocationsFromSubagents', "Allow subagents to invoke subagents."), diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index eeba510b72b6e..0f8c8034f1b12 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -48,6 +48,7 @@ export enum ChatConfiguration { ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled', ChatContextUsageEnabled = 'chat.contextUsage.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', + GeneralPurposeAgentEnabled = 'chat.generalPurposeAgent.enabled', SubagentsAllowInvocationsFromSubagents = 'chat.subagents.allowInvocationsFromSubagents', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', @@ -196,3 +197,9 @@ export const ChatEditorTitleMaxLength = 30; export const CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES = 1000; export const CONTEXT_MODELS_EDITOR = new RawContextKey('inModelsEditor', false); export const CONTEXT_MODELS_SEARCH_FOCUS = new RawContextKey('inModelsSearch', false); + +/** + * The built-in general-purpose agent name. When the model uses this name, + * the subagent inherits the parent's system prompt, model, and tools. + */ +export const GeneralPurposeAgentName = 'General Purpose'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 4849423cd9607..1b6fed663b4c6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -27,7 +27,7 @@ import { ParsedPromptFile } from './promptFileParser.js'; import { AgentInstructionFileType, IAgentSkill, ICustomAgent, IInstructionFile, IPromptsService } from './service/promptsService.js'; import { AGENT_DEBUG_LOG_ENABLED_SETTING, AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING, TROUBLESHOOT_SKILL_PATH } from './promptTypes.js'; import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; -import { ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../constants.js'; import { UserSelectedTools } from '../participants/chatAgents.js'; import { hash } from '../../../../../base/common/hash.js'; import { IAgentPlugin, IAgentPluginService } from '../plugins/agentPluginService.js'; @@ -431,7 +431,10 @@ export class ComputeAutomaticInstructions { entries.push('', '', ''); // add trailing newline } } - if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + if (runSubagentTool) { + const generalPurposeAgentEnabled = !!this._configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + + const customAgentsEnabled = !!this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); const canUseAgent = (() => { if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { return (agent: ICustomAgent) => agent.visibility.agentInvocable; @@ -440,12 +443,22 @@ export class ComputeAutomaticInstructions { return (agent: ICustomAgent) => subagents.includes(agent.name); } })(); - const agents = await this._promptsService.getCustomAgents(token); - if (agents.length > 0) { + const agents = customAgentsEnabled ? await this._promptsService.getCustomAgents(token) : []; + + if (generalPurposeAgentEnabled || 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.`); + + if (generalPurposeAgentEnabled) { + // Built-in General Purpose agent, always available when experiment is on + entries.push(''); + entries.push(`${GeneralPurposeAgentName}`); + entries.push(`Full-capability agent for complex multi-step tasks requiring high-quality reasoning. Has access to the same tools and capabilities as the current agent and inherits the parent agent's model and system prompt. Use for tasks that don't fit a more specialized agent.`); + entries.push(''); + } + for (const agent of agents) { if (canUseAgent(agent)) { entries.push(''); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 92b43a4993632..f953732e3414f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -5,20 +5,20 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Event } from '../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; -import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; import { IChatProgress, IChatService } from '../../chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js'; import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../participants/chatAgents.js'; @@ -50,7 +50,8 @@ const BaseModelDescription = `Launch a new agent to handle complex, multi-step t - When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. - Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. - The agent's outputs should generally be trusted -- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent`; +- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent +- If the user asks for a certain agent, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; export interface IRunSubagentToolInputParams { prompt: string; @@ -64,7 +65,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { static readonly Id = 'runSubagent'; - readonly onDidUpdateToolData: Event; + private readonly _onDidUpdateToolData = this._register(new Emitter()); + readonly onDidUpdateToolData: Event = this._onDidUpdateToolData.event; /** Hack to port data between prepare/invoke */ private readonly _resolvedModels = new Map(); @@ -78,42 +80,54 @@ export class RunSubagentTool extends Disposable implements IToolImpl { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILogService private readonly logService: ILogService, - @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, @IConfigurationService private readonly configurationService: IConfigurationService, @IPromptsService private readonly promptsService: IPromptsService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, ) { super(); - this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => - e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) - ); + + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => + e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) || + e.affectsConfiguration(ChatConfiguration.GeneralPurposeAgentEnabled) + )(() => this._onDidUpdateToolData.fire())); } getToolData(): IToolData { - let modelDescription = BaseModelDescription; - const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { - type: 'object', - properties: { - prompt: { - type: 'string', - description: 'A detailed description of the task for the agent to perform' - }, - description: { - type: 'string', - description: 'A short (3-5 word) description of the task' - } + const modelDescription = BaseModelDescription; + const generalPurposeAgentEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + + const properties: IJSONSchemaMap = { + prompt: { + type: 'string', + description: 'A detailed description of the task for the agent to perform' }, - required: ['prompt', 'description'] + description: { + type: 'string', + description: 'A short (3-5 word) description of the task' + } }; - if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { - inputSchema.properties.agentName = { + if (customAgentsEnabled || generalPurposeAgentEnabled) { + properties.agentName = { type: 'string', - description: 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' + description: generalPurposeAgentEnabled + ? 'Name of the agent to invoke.' + : '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, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; } + + const required: string[] = ['prompt', 'description']; + if (generalPurposeAgentEnabled) { + required.push('agentName'); + } + + const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { + type: 'object', + properties, + required + }; const runSubagentToolData: IToolData = { id: RunSubagentTool.Id, toolReferenceName: VSCodeToolReference.runSubagent, @@ -161,8 +175,14 @@ export class RunSubagentTool extends Disposable implements IToolImpl { let resolvedModelName: string | undefined; const subAgentName = args.agentName; - if (subAgentName) { - subagent = await this.getSubAgentByName(subAgentName); + // Defensive: model may omit agentName despite schema requiring it + const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + const isGeneralPurpose = gpEnabled && (!subAgentName || subAgentName === GeneralPurposeAgentName); + const effectiveSubAgentName = isGeneralPurpose ? GeneralPurposeAgentName : subAgentName; + + if (subAgentName && !isGeneralPurpose) { + subagent = customAgentsEnabled ? await this.getSubAgentByName(subAgentName) : undefined; if (subagent) { // Check the pre-resolved model cache from prepareToolInvocation const cached = this._resolvedModels.get(invocation.callId); @@ -195,12 +215,15 @@ export class RunSubagentTool extends Disposable implements IToolImpl { modeInstructions = instructions && { name: subAgentName, content: instructions.content, - toolReferences: this.toolsService.toToolReferences(instructions.toolReferences), + toolReferences: this.languageModelToolsService.toToolReferences(instructions.toolReferences), metadata: instructions.metadata, isBuiltin: isBuiltinAgent(subagent.source, subagent.uri, this.productService), }; } else { - throw new Error(`Requested agent '${subAgentName}' not found. Try again with the correct agent name, or omit the agentName to use the current agent.`); + this._resolvedModels.delete(invocation.callId); + const baseHint = ' Try again with the correct agent name, or omit agentName to use the current agent.'; + const gpHint = gpEnabled ? ` Additionally, you can use '${GeneralPurposeAgentName}' for a full-capability agent.` : ''; + throw new Error(`Requested agent '${subAgentName}' not found.${baseHint}${gpHint}`); } } else { // No subagent name - clean up any cached entry and resolve model name from main model @@ -312,7 +335,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { variables: { variables: variableSet.asArray() }, location: ChatAgentLocation.Chat, subAgentInvocationId: subAgentInvocationId, - subAgentName: subAgentName, + subAgentName: effectiveSubAgentName, userSelectedModelId: modeModelId, modelConfiguration: modeModelId ? this.languageModelsService.getModelConfiguration(modeModelId) : undefined, userSelectedTools: modeTools, @@ -433,7 +456,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const args = context.parameters as IRunSubagentToolInputParams; - const subagent = args.agentName ? await this.getSubAgentByName(args.agentName) : undefined; + // Defensive: model may omit agentName despite schema requiring it + const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const customAgentsEnabled = this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); + const isGeneralPurpose = gpEnabled && (!args.agentName || args.agentName === GeneralPurposeAgentName); + const subagent = (args.agentName && !isGeneralPurpose && customAgentsEnabled) ? await this.getSubAgentByName(args.agentName) : undefined; // Resolve the model early and cache it for invoke() const resolved = this.resolveSubagentModel(subagent, context.modelId); @@ -444,7 +471,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { toolSpecificData: { kind: 'subagent', description: args.description, - agentName: subagent?.name, + agentName: isGeneralPurpose ? GeneralPurposeAgentName : (subagent?.name ?? args.agentName), prompt: args.prompt, modelName: resolved.resolvedModelName, }, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index af0c6acb7cc0d..7c6aa3fd2ea58 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -45,7 +45,7 @@ import { ILanguageModelToolsService } from '../../../common/tools/languageModelT import { IRemoteAgentService } from '../../../../../../workbench/services/remote/common/remoteAgentService.js'; import { basename } from '../../../../../../base/common/resources.js'; import { match } from '../../../../../../base/common/glob.js'; -import { ChatModeKind } from '../../../common/constants.js'; +import { ChatModeKind, GeneralPurposeAgentName } from '../../../common/constants.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; @@ -1505,6 +1505,87 @@ suite('ComputeAutomaticInstructions', () => { assert.equal(xmlContents(agents[2], 'name')[0], `test-agent-5`); }); + test('should include General Purpose agent first when experiment is enabled', async () => { + const rootFolderName = 'gp-agents-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true); + + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); + + testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { + [AGENTS_SOURCE_FOLDER]: true, + }); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/test-agent-1.agent.md`, + contents: [ + '---', + 'description: \'Test agent 1\'', + '---', + 'Test agent content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'], + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for agents list'); + + const agentsList = xmlContents(textVariables[0].value, 'agents'); + assert.equal(agentsList.length, 1, 'There should be one agents list'); + + const agents = xmlContents(agentsList[0], 'agent'); + assert.equal(agents.length, 2, 'There should be two agents (General Purpose + 1 custom)'); + + // First agent should always be the built-in General Purpose agent + assert.equal(xmlContents(agents[0], 'name')[0], GeneralPurposeAgentName); + + assert.equal(xmlContents(agents[1], 'name')[0], 'test-agent-1'); + assert.equal(xmlContents(agents[1], 'description')[0], 'Test agent 1'); + }); + + test('should include General Purpose agent even without custom agents config', async () => { + workspaceContextService.setWorkspace(testWorkspace(URI.file('/gp-only-test'))); + + // Explicitly do NOT set chat.customAgentInSubagent.enabled + + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'], + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for agents list'); + + const agentsList = xmlContents(textVariables[0].value, 'agents'); + assert.equal(agentsList.length, 1, 'There should be one agents list'); + + const agents = xmlContents(agentsList[0], 'agent'); + assert.equal(agents.length, 1, 'There should be only the GP agent'); + assert.equal(xmlContents(agents[0], 'name')[0], GeneralPurposeAgentName); + }); + test('should include skills list when readFile tool available', async () => { const rootFolderName = 'skills-list-test'; const rootFolder = `/${rootFolderName}`; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index fdc8346147587..19fb1f155f8b3 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -22,7 +22,7 @@ import { MockPromptsService } from '../../promptSyntax/service/mockPromptsServic import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; import { IToolInvocation, ToolProgress } from '../../../../common/tools/languageModelToolsService.js'; import { IChatModel } from '../../../../common/model/chatModel.js'; -import { ChatConfiguration } from '../../../../common/constants.js'; +import { ChatConfiguration, GeneralPurposeAgentName } from '../../../../common/constants.js'; suite('RunSubagentTool', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +50,6 @@ suite('RunSubagentTool', () => { suite('prepareToolInvocation', () => { test('returns correct toolSpecificData', async () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); const customMode: ICustomAgent = { @@ -71,8 +70,7 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, @@ -101,12 +99,124 @@ suite('RunSubagentTool', () => { modelName: undefined, }); }); + + function createToolWithGP(opts?: { customAgents?: ICustomAgent[] }) { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const promptsService = new MockPromptsService(); + if (opts?.customAgents) { + promptsService.setCustomModes(opts.customAgents); + } + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), + promptsService, + {} as IInstantiationService, + {} as IProductService, + )); + return tool; + } + + async function createToolWithGPReady(opts?: { customAgents?: ICustomAgent[] }) { + return createToolWithGP(opts); + } + + test('treats undefined agentName as General Purpose when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: undefined }, + toolCallId: 'test-call-undef', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('treats empty string agentName as General Purpose when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: '' }, + toolCallId: 'test-call-empty', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('treats explicit General Purpose agentName as GP path', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: GeneralPurposeAgentName }, + toolCallId: 'test-call-gp', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: GeneralPurposeAgentName, + prompt: 'Test prompt', + modelName: undefined, + }); + }); + + test('passes through unknown agentName when experiment is enabled', async () => { + const tool = await createToolWithGPReady(); + + const result = await tool.prepareToolInvocation( + { + parameters: { prompt: 'Test prompt', description: 'Test task', agentName: 'NonExistentAgent' }, + toolCallId: 'test-call-unknown', + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: 'NonExistentAgent', + prompt: 'Test prompt', + modelName: undefined, + }); + }); }); suite('getToolData', () => { test('returns basic tool data', () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); const tool = testDisposables.add(new RunSubagentTool( @@ -115,8 +225,7 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, @@ -128,14 +237,12 @@ suite('RunSubagentTool', () => { assert.ok(toolData.inputSchema); assert.ok(toolData.inputSchema.properties?.prompt); assert.ok(toolData.inputSchema.properties?.description); + assert.strictEqual(toolData.inputSchema.properties?.agentName, undefined, 'agentName should not be in schema when neither GP nor custom agents is enabled'); assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description']); }); - test('includes agentName property when SubagentToolCustomAgents is enabled', () => { + test('marks agentName as required when GP experiment is enabled', async () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService({ - 'chat.customAgentInSubagent.enabled': true, - }); const promptsService = new MockPromptsService(); const tool = testDisposables.add(new RunSubagentTool( @@ -144,16 +251,15 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), promptsService, {} as IInstantiationService, {} as IProductService, )); const toolData = tool.getToolData(); - - assert.ok(toolData.inputSchema?.properties?.agentName, 'agentName should be in schema when custom agents enabled'); + assert.ok(toolData.inputSchema?.properties?.agentName); + assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description', 'agentName']); }); }); @@ -244,7 +350,6 @@ suite('RunSubagentTool', () => { customAgents?: ICustomAgent[]; }) { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); - const configService = new TestConfigurationService(); const promptsService = new MockPromptsService(); if (opts.customAgents) { promptsService.setCustomModes(opts.customAgents); @@ -265,8 +370,7 @@ suite('RunSubagentTool', () => { mockToolsService, mockLanguageModelsService as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService({ [ChatConfiguration.SubagentToolCustomAgents]: true }), promptsService, {} as IInstantiationService, {} as IProductService, @@ -542,7 +646,6 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, configService, promptsService, mockInstantiationService as IInstantiationService,