From c9b1a35dda0a13b36b749c5c4c76ae938371dd08 Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 10:20:38 -0700 Subject: [PATCH 1/9] Add General Purpose agent support behind experiment Add a built-in 'General Purpose' agent to the runSubagent tool, gated behind the 'chat.generalPurposeAgent' experiment treatment: - Add GeneralPurposeAgentName constant - Make agentName required and route undefined/GP names to built-in agent - Render GP agent in automatic instructions agents block - Clean up duplicate DI injection in RunSubagentTool - Add unit tests for GP agent paths --- .../contrib/chat/common/constants.ts | 6 + .../computeAutomaticInstructions.ts | 34 +++- .../tools/builtinTools/runSubagentTool.ts | 66 ++++--- .../computeAutomaticInstructions.test.ts | 69 ++++++- .../service/promptsService.test.ts | 14 +- .../builtinTools/runSubagentTool.test.ts | 170 ++++++++++++++++-- 6 files changed, 307 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index eeba510b72b6e..b1bf38c4ff5d2 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -196,3 +196,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..4f01070cfef12 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -27,10 +27,11 @@ 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'; +import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; export type InstructionsCollectionEvent = { applyingInstructionsCount: number; @@ -78,6 +79,7 @@ export class ComputeAutomaticInstructions { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, + @IWorkbenchAssignmentService private readonly _assignmentService: IWorkbenchAssignmentService, ) { } @@ -432,6 +434,21 @@ export class ComputeAutomaticInstructions { } } if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + const generalPurposeAgentEnabled = !!(await this._assignmentService.getTreatment('chat.generalPurposeAgent')); + + if (generalPurposeAgentEnabled) { + 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.`); + + // Built-in General Purpose agent, always available + entries.push(''); + entries.push(`${GeneralPurposeAgentName}`); + entries.push(`Full-capability agent for complex multi-step tasks requiring the complete toolset and high-quality reasoning. Has access to all tools. Inherits the parent agent's model and system prompt. Use for tasks that don't fit a more specialized agent.`); + entries.push(''); + } + const canUseAgent = (() => { if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { return (agent: ICustomAgent) => agent.visibility.agentInvocable; @@ -441,11 +458,16 @@ export class ComputeAutomaticInstructions { } })(); 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.`); + if (!generalPurposeAgentEnabled && agents.length === 0) { + // No agents to show at all when experiment is off and no custom agents + } else { + if (!generalPurposeAgentEnabled) { + // Only render the agents block header when not already rendered by GP + 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 (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..4f6a8c1163568 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -12,13 +12,14 @@ import { Disposable, DisposableStore } from '../../../../../../base/common/lifec 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 { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.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 +51,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 +66,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { static readonly Id = 'runSubagent'; - readonly onDidUpdateToolData: Event; + readonly onDidUpdateToolData: Event; /** Hack to port data between prepare/invoke */ private readonly _resolvedModels = new Map(); @@ -72,26 +74,42 @@ export class RunSubagentTool extends Disposable implements IToolImpl { /** Tracks the current subagent nesting depth per session to detect and limit recursion. */ private readonly _sessionDepth = new Map(); + /** Cached experiment value for whether the General Purpose agent is enabled. */ + private _generalPurposeAgentEnabled = false; + constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatService private readonly chatService: IChatService, @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, + @IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService, ) { super(); - this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => + + const configEvent = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) ); + const refetchEvent = Event.map(this.assignmentService.onDidRefetchAssignments, () => { }); + this.onDidUpdateToolData = Event.any(configEvent as Event, refetchEvent); + + // Resolve the experiment value asynchronously and re-resolve on refetch + this._resolveExperiment(); + this._register(this.assignmentService.onDidRefetchAssignments(() => this._resolveExperiment())); + } + + private _resolveExperiment(): void { + this.assignmentService.getTreatment('chat.generalPurposeAgent').then(value => { + this._generalPurposeAgentEnabled = !!value; + }); } getToolData(): IToolData { - let modelDescription = BaseModelDescription; + const modelDescription = BaseModelDescription; const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: { @@ -102,18 +120,18 @@ export class RunSubagentTool extends Disposable implements IToolImpl { description: { type: 'string', description: 'A short (3-5 word) description of the task' + }, + agentName: { + type: 'string', + description: this._generalPurposeAgentEnabled + ? 'Name of the agent to invoke.' + : 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' } }, - required: ['prompt', 'description'] + required: this._generalPurposeAgentEnabled + ? ['prompt', 'description', 'agentName'] + : ['prompt', 'description'] }; - - if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { - inputSchema.properties.agentName = { - type: 'string', - 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, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; - } const runSubagentToolData: IToolData = { id: RunSubagentTool.Id, toolReferenceName: VSCodeToolReference.runSubagent, @@ -161,7 +179,10 @@ export class RunSubagentTool extends Disposable implements IToolImpl { let resolvedModelName: string | undefined; const subAgentName = args.agentName; - if (subAgentName) { + // Defensive: model may omit agentName despite schema requiring it + const isGeneralPurpose = this._generalPurposeAgentEnabled && (!subAgentName || subAgentName === GeneralPurposeAgentName); + + if (subAgentName && !isGeneralPurpose) { subagent = await this.getSubAgentByName(subAgentName); if (subagent) { // Check the pre-resolved model cache from prepareToolInvocation @@ -195,12 +216,13 @@ 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); + throw new Error(`Requested agent '${subAgentName}' not found. Use '${GeneralPurposeAgentName}' for a full-capability agent.`); } } else { // No subagent name - clean up any cached entry and resolve model name from main model @@ -433,7 +455,9 @@ 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 isGeneralPurpose = this._generalPurposeAgentEnabled && (!args.agentName || args.agentName === GeneralPurposeAgentName); + const subagent = (args.agentName && !isGeneralPurpose) ? await this.getSubAgentByName(args.agentName) : undefined; // Resolve the model early and cache it for invoke() const resolved = this.resolveSubagentModel(subagent, context.modelId); @@ -444,7 +468,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..a9b9b21bc6a30 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,11 +45,13 @@ 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'; import { observableValue } from '../../../../../../base/common/observable.js'; +import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; +import { NullWorkbenchAssignmentService } from '../../../../../services/assignment/test/common/nullAssignmentService.js'; suite('ComputeAutomaticInstructions', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -193,6 +195,8 @@ suite('ComputeAutomaticInstructions', () => { enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { }, remove: () => { } }, }); + instaService.stub(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -1505,6 +1509,69 @@ 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); + + // Override the assignment service to enable the GP experiment + instaService.stub(IWorkbenchAssignmentService, { + _serviceBrand: undefined, + onDidRefetchAssignments: Event.None, + async getCurrentExperiments() { return []; }, + async getTreatment(name: string): Promise { + return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; + }, + addTelemetryAssignmentFilter() { }, + } as IWorkbenchAssignmentService); + + 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 }, + ['*'], + undefined + ); + 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 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/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index ba7c466db5889..f456dd2b4f074 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 @@ -55,6 +55,8 @@ import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../.. import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginInstruction, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; import { IWorkspaceTrustManagementService } from '../../../../../../../platform/workspace/common/workspaceTrust.js'; +import { IWorkbenchAssignmentService } from '../../../../../../services/assignment/common/assignmentService.js'; +import { NullWorkbenchAssignmentService } from '../../../../../../services/assignment/test/common/nullAssignmentService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -179,6 +181,8 @@ suite('PromptsService', () => { enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { }, remove: () => { } }, }); + instaService.stub(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -299,8 +303,8 @@ suite('PromptsService', () => { assert.deepEqual( result1.body.variableReferences, [ - { name: 'my-tool', range: new Range(10, 10, 10, 17), offset: 240, fullLength: 13 }, - { name: 'my-other-tool', range: new Range(11, 10, 11, 23), offset: 257, fullLength: 19 }, + { name: 'my-tool', range: new Range(10, 10, 10, 17), offset: 240 }, + { name: 'my-other-tool', range: new Range(11, 10, 11, 23), offset: 257 }, ] ); @@ -846,7 +850,7 @@ suite('PromptsService', () => { tools: ['tool1', 'tool2'], agentInstructions: { content: 'Do it with #tool:tool1', - toolReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 22 } }], + toolReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 17 } }], metadata: undefined }, handOffs: undefined, @@ -864,8 +868,8 @@ suite('PromptsService', () => { agentInstructions: { content: 'First use #tool:tool2\nThen use #tool:tool1', toolReferences: [ - { name: 'tool1', range: { start: 31, endExclusive: 42 } }, - { name: 'tool2', range: { start: 10, endExclusive: 21 } } + { name: 'tool1', range: { start: 31, endExclusive: 37 } }, + { name: 'tool2', range: { start: 10, endExclusive: 16 } } ], metadata: undefined }, 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..4feeade1027cd 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,10 @@ 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'; +import { NullWorkbenchAssignmentService } from '../../../../../../services/assignment/test/common/nullAssignmentService.js'; +import { IWorkbenchAssignmentService } from '../../../../../../services/assignment/common/assignmentService.js'; +import { Event } from '../../../../../../../base/common/event.js'; suite('RunSubagentTool', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +53,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,11 +73,11 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, + new NullWorkbenchAssignmentService(), )); const result = await tool.prepareToolInvocation( @@ -101,12 +103,133 @@ 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 assignmentService: IWorkbenchAssignmentService = { + _serviceBrand: undefined, + onDidRefetchAssignments: Event.None, + async getCurrentExperiments() { return []; }, + async getTreatment(name: string): Promise { + return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; + }, + addTelemetryAssignmentFilter() { }, + }; + + return testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + new TestConfigurationService(), + promptsService, + {} as IInstantiationService, + {} as IProductService, + assignmentService, + )); + } + + test('treats undefined agentName as General Purpose when experiment is enabled', async () => { + const tool = createToolWithGP(); + await new Promise(resolve => setTimeout(resolve, 0)); + + 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 = createToolWithGP(); + await new Promise(resolve => setTimeout(resolve, 0)); + + 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 = createToolWithGP(); + await new Promise(resolve => setTimeout(resolve, 0)); + + 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 = createToolWithGP(); + await new Promise(resolve => setTimeout(resolve, 0)); + + 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,11 +238,11 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, + new NullWorkbenchAssignmentService(), )); const toolData = tool.getToolData(); @@ -128,15 +251,22 @@ suite('RunSubagentTool', () => { assert.ok(toolData.inputSchema); assert.ok(toolData.inputSchema.properties?.prompt); assert.ok(toolData.inputSchema.properties?.description); + assert.ok(toolData.inputSchema.properties?.agentName); 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 assignmentService: IWorkbenchAssignmentService = { + _serviceBrand: undefined, + onDidRefetchAssignments: Event.None, + async getCurrentExperiments() { return []; }, + async getTreatment(name: string): Promise { + return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; + }, + addTelemetryAssignmentFilter() { }, + }; const tool = testDisposables.add(new RunSubagentTool( {} as IChatAgentService, @@ -144,16 +274,19 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, + assignmentService, )); - const toolData = tool.getToolData(); + // Wait a tick for the constructor's async experiment resolution + await new Promise(resolve => setTimeout(resolve, 0)); - assert.ok(toolData.inputSchema?.properties?.agentName, 'agentName should be in schema when custom agents enabled'); + const toolData = tool.getToolData(); + assert.ok(toolData.inputSchema?.properties?.agentName); + assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description', 'agentName']); }); }); @@ -244,7 +377,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,11 +397,11 @@ suite('RunSubagentTool', () => { mockToolsService, mockLanguageModelsService as ILanguageModelsService, new NullLogService(), - mockToolsService, - configService, + new TestConfigurationService(), promptsService, {} as IInstantiationService, {} as IProductService, + new NullWorkbenchAssignmentService(), )); return tool; @@ -542,11 +674,11 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - mockToolsService, configService, promptsService, mockInstantiationService as IInstantiationService, {} as IProductService, + new NullWorkbenchAssignmentService(), )); return { tool, mockChatAgentService }; From 517a0e1e677a9303baec30899713105895a5dded Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 10:37:27 -0700 Subject: [PATCH 2/9] Address PR feedback: fix Event cast, add experiment Emitter, try/catch, deterministic tests - Replace unsafe 'configEvent as Event' cast with dedicated Emitter (fixes tsgo typecheck CI failure) - Fire onDidUpdateToolData when experiment resolution changes the value - Add try/catch around getTreatment in computeAutomaticInstructions - Replace flaky setTimeout(0) in tests with Event.toPromise(onDidUpdateToolData) --- .../computeAutomaticInstructions.ts | 7 +++++- .../tools/builtinTools/runSubagentTool.ts | 15 ++++++----- .../builtinTools/runSubagentTool.test.ts | 25 +++++++++++-------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 4f01070cfef12..1bbb7860c1347 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -434,7 +434,12 @@ export class ComputeAutomaticInstructions { } } if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { - const generalPurposeAgentEnabled = !!(await this._assignmentService.getTreatment('chat.generalPurposeAgent')); + let generalPurposeAgentEnabled = false; + try { + generalPurposeAgentEnabled = !!(await this._assignmentService.getTreatment('chat.generalPurposeAgent')); + } catch (error) { + this._logService.error('[AutomaticInstructions] Failed to resolve treatment chat.generalPurposeAgent', error); + } if (generalPurposeAgentEnabled) { 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 4f6a8c1163568..865e62c708a1c 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -5,7 +5,7 @@ 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'; @@ -66,7 +66,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(); @@ -91,11 +92,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { ) { super(); - const configEvent = Event.filter(this.configurationService.onDidChangeConfiguration, e => + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) - ); - const refetchEvent = Event.map(this.assignmentService.onDidRefetchAssignments, () => { }); - this.onDidUpdateToolData = Event.any(configEvent as Event, refetchEvent); + )((() => this._onDidUpdateToolData.fire()))); // Resolve the experiment value asynchronously and re-resolve on refetch this._resolveExperiment(); @@ -104,7 +103,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl { private _resolveExperiment(): void { this.assignmentService.getTreatment('chat.generalPurposeAgent').then(value => { + const changed = this._generalPurposeAgentEnabled !== !!value; this._generalPurposeAgentEnabled = !!value; + if (changed) { + this._onDidUpdateToolData.fire(); + } }); } 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 4feeade1027cd..bf184f2dba376 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 @@ -120,7 +120,7 @@ suite('RunSubagentTool', () => { addTelemetryAssignmentFilter() { }, }; - return testDisposables.add(new RunSubagentTool( + const tool = testDisposables.add(new RunSubagentTool( {} as IChatAgentService, {} as IChatService, mockToolsService, @@ -132,11 +132,17 @@ suite('RunSubagentTool', () => { {} as IProductService, assignmentService, )); + return tool; + } + + async function createToolWithGPReady(opts?: { customAgents?: ICustomAgent[] }) { + const tool = createToolWithGP(opts); + await Event.toPromise(tool.onDidUpdateToolData); + return tool; } test('treats undefined agentName as General Purpose when experiment is enabled', async () => { - const tool = createToolWithGP(); - await new Promise(resolve => setTimeout(resolve, 0)); + const tool = await createToolWithGPReady(); const result = await tool.prepareToolInvocation( { @@ -158,8 +164,7 @@ suite('RunSubagentTool', () => { }); test('treats empty string agentName as General Purpose when experiment is enabled', async () => { - const tool = createToolWithGP(); - await new Promise(resolve => setTimeout(resolve, 0)); + const tool = await createToolWithGPReady(); const result = await tool.prepareToolInvocation( { @@ -181,8 +186,7 @@ suite('RunSubagentTool', () => { }); test('treats explicit General Purpose agentName as GP path', async () => { - const tool = createToolWithGP(); - await new Promise(resolve => setTimeout(resolve, 0)); + const tool = await createToolWithGPReady(); const result = await tool.prepareToolInvocation( { @@ -204,8 +208,7 @@ suite('RunSubagentTool', () => { }); test('passes through unknown agentName when experiment is enabled', async () => { - const tool = createToolWithGP(); - await new Promise(resolve => setTimeout(resolve, 0)); + const tool = await createToolWithGPReady(); const result = await tool.prepareToolInvocation( { @@ -281,8 +284,8 @@ suite('RunSubagentTool', () => { assignmentService, )); - // Wait a tick for the constructor's async experiment resolution - await new Promise(resolve => setTimeout(resolve, 0)); + // Wait for the constructor's async experiment resolution via onDidUpdateToolData + await Event.toPromise(tool.onDidUpdateToolData); const toolData = tool.getToolData(); assert.ok(toolData.inputSchema?.properties?.agentName); From d16d38409d106a12ea53cbce1b59f4435d483660 Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 10:45:40 -0700 Subject: [PATCH 3/9] Fix merge regression: restore fullLength in toolReferences range calculation The merge conflict resolution incorrectly replaced fullLength with name.length + 1 for toolReference OffsetRange calculation, and removed fullLength from variableReferences test expectations. Restore the original behavior from main. --- .../common/promptSyntax/service/promptsService.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 f456dd2b4f074..d1659fc457fc9 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 @@ -303,8 +303,8 @@ suite('PromptsService', () => { assert.deepEqual( result1.body.variableReferences, [ - { name: 'my-tool', range: new Range(10, 10, 10, 17), offset: 240 }, - { name: 'my-other-tool', range: new Range(11, 10, 11, 23), offset: 257 }, + { name: 'my-tool', range: new Range(10, 10, 10, 17), offset: 240, fullLength: 13 }, + { name: 'my-other-tool', range: new Range(11, 10, 11, 23), offset: 257, fullLength: 19 }, ] ); @@ -850,7 +850,7 @@ suite('PromptsService', () => { tools: ['tool1', 'tool2'], agentInstructions: { content: 'Do it with #tool:tool1', - toolReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 17 } }], + toolReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 22 } }], metadata: undefined }, handOffs: undefined, @@ -868,8 +868,8 @@ suite('PromptsService', () => { agentInstructions: { content: 'First use #tool:tool2\nThen use #tool:tool1', toolReferences: [ - { name: 'tool1', range: { start: 31, endExclusive: 37 } }, - { name: 'tool2', range: { start: 10, endExclusive: 16 } } + { name: 'tool1', range: { start: 31, endExclusive: 42 } }, + { name: 'tool2', range: { start: 10, endExclusive: 21 } } ], metadata: undefined }, From 88cc9189302db8f009c7dd97f580c4c5799bba0b Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 11:19:21 -0700 Subject: [PATCH 4/9] Apply critical review fixes - Add error handler to _resolveExperiment() preventing unhandled promise rejections when getTreatment fails - Decouple GP agent from SubagentToolCustomAgents config gate so experiment works independently of custom agents setting - Fix redundant parens on Event listener arrow function - Add test for GP agent rendering without custom agents config --- .../computeAutomaticInstructions.ts | 42 ++++++++----------- .../tools/builtinTools/runSubagentTool.ts | 4 +- .../computeAutomaticInstructions.test.ts | 38 +++++++++++++++++ 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 1bbb7860c1347..6515238e5a611 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -433,7 +433,7 @@ export class ComputeAutomaticInstructions { entries.push('', '', ''); // add trailing newline } } - if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + if (runSubagentTool) { let generalPurposeAgentEnabled = false; try { generalPurposeAgentEnabled = !!(await this._assignmentService.getTreatment('chat.generalPurposeAgent')); @@ -441,19 +441,7 @@ export class ComputeAutomaticInstructions { this._logService.error('[AutomaticInstructions] Failed to resolve treatment chat.generalPurposeAgent', error); } - if (generalPurposeAgentEnabled) { - 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.`); - - // Built-in General Purpose agent, always available - entries.push(''); - entries.push(`${GeneralPurposeAgentName}`); - entries.push(`Full-capability agent for complex multi-step tasks requiring the complete toolset and high-quality reasoning. Has access to all tools. Inherits the parent agent's model and system prompt. Use for tasks that don't fit a more specialized agent.`); - entries.push(''); - } - + const customAgentsEnabled = !!this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); const canUseAgent = (() => { if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { return (agent: ICustomAgent) => agent.visibility.agentInvocable; @@ -462,17 +450,23 @@ export class ComputeAutomaticInstructions { return (agent: ICustomAgent) => subagents.includes(agent.name); } })(); - const agents = await this._promptsService.getCustomAgents(token); - if (!generalPurposeAgentEnabled && agents.length === 0) { - // No agents to show at all when experiment is off and no custom agents - } else { - if (!generalPurposeAgentEnabled) { - // Only render the agents block header when not already rendered by GP - 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.`); + 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 the complete toolset and high-quality reasoning. Has access to all tools. 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 865e62c708a1c..6a1e0e491e413 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -94,7 +94,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) - )((() => this._onDidUpdateToolData.fire()))); + )(() => this._onDidUpdateToolData.fire())); // Resolve the experiment value asynchronously and re-resolve on refetch this._resolveExperiment(); @@ -108,6 +108,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { if (changed) { this._onDidUpdateToolData.fire(); } + }, (error: unknown) => { + this.logService.error(`[RunSubagentTool] Failed to resolve treatment chat.generalPurposeAgent: ${error}`); }); } 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 a9b9b21bc6a30..0e46653aa76ba 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 @@ -1572,6 +1572,44 @@ suite('ComputeAutomaticInstructions', () => { 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 + + // Override the assignment service to enable the GP experiment + instaService.stub(IWorkbenchAssignmentService, { + _serviceBrand: undefined, + onDidRefetchAssignments: Event.None, + async getCurrentExperiments() { return []; }, + async getTreatment(name: string): Promise { + return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; + }, + addTelemetryAssignmentFilter() { }, + } as IWorkbenchAssignmentService); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'], + undefined + ); + 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}`; From 8cfb2990ae2e13a25cccfb712614f507fb936fb7 Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 11:46:37 -0700 Subject: [PATCH 5/9] Remove unrelated promptsServiceImpl refactor from PR Reset promptsServiceImpl.ts and promptsService.test.ts back to main. These files contained an unrelated refactor (method renames, type simplifications) that was accidentally carried over during the port from PR #295494. --- .../test/common/promptSyntax/service/promptsService.test.ts | 4 ---- 1 file changed, 4 deletions(-) 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 d1659fc457fc9..ba7c466db5889 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 @@ -55,8 +55,6 @@ import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../.. import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginInstruction, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js'; import { IWorkspaceTrustManagementService } from '../../../../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchAssignmentService } from '../../../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../../../services/assignment/test/common/nullAssignmentService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -181,8 +179,6 @@ suite('PromptsService', () => { enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { }, remove: () => { } }, }); - instaService.stub(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); - service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); From fb5f8c4c3c4fe4cd6675cd469eebea20aa614dd5 Mon Sep 17 00:00:00 2001 From: digitarald Date: Tue, 31 Mar 2026 11:51:54 -0700 Subject: [PATCH 6/9] Fix GP agent description: inherit parent tools, not 'all tools' --- .../chat/common/promptSyntax/computeAutomaticInstructions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 6515238e5a611..d8010b326125f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -462,7 +462,7 @@ export class ComputeAutomaticInstructions { // 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 the complete toolset and high-quality reasoning. Has access to all tools. Inherits the parent agent's model and system prompt. Use for tasks that don't fit a more specialized agent.`); + 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(''); } } From 4a53c8dc99ea2f541bbf14b1c3ca0b5c29d5225c Mon Sep 17 00:00:00 2001 From: digitarald Date: Wed, 1 Apr 2026 23:28:56 -0400 Subject: [PATCH 7/9] Refactor GP agent from experiment to experimental setting Replace direct IWorkbenchAssignmentService.getTreatment checks with an experimental configuration setting chat.generalPurposeAgent.enabled. This simplifies runtime code from async experiment resolution to synchronous configurationService.getValue() calls. - Add GeneralPurposeAgentEnabled to ChatConfiguration enum - Register setting with experiment: { mode: 'auto' }, default: false - Remove IWorkbenchAssignmentService DI from RunSubagentTool and ComputeAutomaticInstructions (zero other usages in either file) - Make GP agent error hint conditional on setting being enabled - Update tests to use config-based setup instead of assignment stubs --- .../contrib/chat/browser/chat.contribution.ts | 9 +++++ .../contrib/chat/common/constants.ts | 1 + .../computeAutomaticInstructions.ts | 9 +---- .../tools/builtinTools/runSubagentTool.ts | 38 ++++++------------- .../computeAutomaticInstructions.test.ts | 26 +------------ .../builtinTools/runSubagentTool.test.ts | 38 ++----------------- 6 files changed, 27 insertions(+), 94 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 06e553b252893..b800c491268f2 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'], + 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 b1bf38c4ff5d2..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', diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index d8010b326125f..3df5a2a1bc77f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -31,7 +31,6 @@ import { ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../con import { UserSelectedTools } from '../participants/chatAgents.js'; import { hash } from '../../../../../base/common/hash.js'; import { IAgentPlugin, IAgentPluginService } from '../plugins/agentPluginService.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; export type InstructionsCollectionEvent = { applyingInstructionsCount: number; @@ -79,7 +78,6 @@ export class ComputeAutomaticInstructions { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, - @IWorkbenchAssignmentService private readonly _assignmentService: IWorkbenchAssignmentService, ) { } @@ -434,12 +432,7 @@ export class ComputeAutomaticInstructions { } } if (runSubagentTool) { - let generalPurposeAgentEnabled = false; - try { - generalPurposeAgentEnabled = !!(await this._assignmentService.getTreatment('chat.generalPurposeAgent')); - } catch (error) { - this._logService.error('[AutomaticInstructions] Failed to resolve treatment chat.generalPurposeAgent', error); - } + const generalPurposeAgentEnabled = !!this._configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); const customAgentsEnabled = !!this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents); const canUseAgent = (() => { 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 6a1e0e491e413..abecd3b1235ca 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -16,7 +16,6 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ 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 { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; import { IChatProgress, IChatService } from '../../chatService/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js'; @@ -75,9 +74,6 @@ export class RunSubagentTool extends Disposable implements IToolImpl { /** Tracks the current subagent nesting depth per session to detect and limit recursion. */ private readonly _sessionDepth = new Map(); - /** Cached experiment value for whether the General Purpose agent is enabled. */ - private _generalPurposeAgentEnabled = false; - constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatService private readonly chatService: IChatService, @@ -88,29 +84,13 @@ export class RunSubagentTool extends Disposable implements IToolImpl { @IPromptsService private readonly promptsService: IPromptsService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, - @IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService, ) { super(); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => - e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) + e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents) || + e.affectsConfiguration(ChatConfiguration.GeneralPurposeAgentEnabled) )(() => this._onDidUpdateToolData.fire())); - - // Resolve the experiment value asynchronously and re-resolve on refetch - this._resolveExperiment(); - this._register(this.assignmentService.onDidRefetchAssignments(() => this._resolveExperiment())); - } - - private _resolveExperiment(): void { - this.assignmentService.getTreatment('chat.generalPurposeAgent').then(value => { - const changed = this._generalPurposeAgentEnabled !== !!value; - this._generalPurposeAgentEnabled = !!value; - if (changed) { - this._onDidUpdateToolData.fire(); - } - }, (error: unknown) => { - this.logService.error(`[RunSubagentTool] Failed to resolve treatment chat.generalPurposeAgent: ${error}`); - }); } getToolData(): IToolData { @@ -128,12 +108,12 @@ export class RunSubagentTool extends Disposable implements IToolImpl { }, agentName: { type: 'string', - description: this._generalPurposeAgentEnabled + description: this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) ? 'Name of the agent to invoke.' : 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' } }, - required: this._generalPurposeAgentEnabled + required: this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) ? ['prompt', 'description', 'agentName'] : ['prompt', 'description'] }; @@ -185,7 +165,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const subAgentName = args.agentName; // Defensive: model may omit agentName despite schema requiring it - const isGeneralPurpose = this._generalPurposeAgentEnabled && (!subAgentName || subAgentName === GeneralPurposeAgentName); + const isGeneralPurpose = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) && (!subAgentName || subAgentName === GeneralPurposeAgentName); if (subAgentName && !isGeneralPurpose) { subagent = await this.getSubAgentByName(subAgentName); @@ -227,7 +207,11 @@ export class RunSubagentTool extends Disposable implements IToolImpl { }; } else { this._resolvedModels.delete(invocation.callId); - throw new Error(`Requested agent '${subAgentName}' not found. Use '${GeneralPurposeAgentName}' for a full-capability agent.`); + const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); + const hint = gpEnabled + ? ` Use '${GeneralPurposeAgentName}' for a full-capability agent.` + : ''; + throw new Error(`Requested agent '${subAgentName}' not found.${hint}`); } } else { // No subagent name - clean up any cached entry and resolve model name from main model @@ -461,7 +445,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const args = context.parameters as IRunSubagentToolInputParams; // Defensive: model may omit agentName despite schema requiring it - const isGeneralPurpose = this._generalPurposeAgentEnabled && (!args.agentName || args.agentName === GeneralPurposeAgentName); + const isGeneralPurpose = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) && (!args.agentName || args.agentName === GeneralPurposeAgentName); const subagent = (args.agentName && !isGeneralPurpose) ? await this.getSubAgentByName(args.agentName) : undefined; // Resolve the model early and cache it for invoke() 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 0e46653aa76ba..eef59d432eb07 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 @@ -50,8 +50,6 @@ import { IContextKeyService } from '../../../../../../platform/contextkey/common import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { IAgentPlugin, IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; import { observableValue } from '../../../../../../base/common/observable.js'; -import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../../services/assignment/test/common/nullAssignmentService.js'; suite('ComputeAutomaticInstructions', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -195,8 +193,6 @@ suite('ComputeAutomaticInstructions', () => { enablementModel: { readEnabled: () => 2 /* EnabledProfile */, setEnabled: () => { }, remove: () => { } }, }); - instaService.stub(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); - service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -1518,16 +1514,7 @@ suite('ComputeAutomaticInstructions', () => { testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true); - // Override the assignment service to enable the GP experiment - instaService.stub(IWorkbenchAssignmentService, { - _serviceBrand: undefined, - onDidRefetchAssignments: Event.None, - async getCurrentExperiments() { return []; }, - async getTreatment(name: string): Promise { - return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; - }, - addTelemetryAssignmentFilter() { }, - } as IWorkbenchAssignmentService); + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { [AGENTS_SOURCE_FOLDER]: true, @@ -1577,16 +1564,7 @@ suite('ComputeAutomaticInstructions', () => { // Explicitly do NOT set chat.customAgentInSubagent.enabled - // Override the assignment service to enable the GP experiment - instaService.stub(IWorkbenchAssignmentService, { - _serviceBrand: undefined, - onDidRefetchAssignments: Event.None, - async getCurrentExperiments() { return []; }, - async getTreatment(name: string): Promise { - return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; - }, - addTelemetryAssignmentFilter() { }, - } as IWorkbenchAssignmentService); + testConfigService.setUserConfiguration('chat.generalPurposeAgent.enabled', true); const contextComputer = instaService.createInstance( ComputeAutomaticInstructions, 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 bf184f2dba376..6a101be5f247a 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 @@ -23,9 +23,6 @@ import { ExtensionIdentifier } from '../../../../../../../platform/extensions/co import { IToolInvocation, ToolProgress } from '../../../../common/tools/languageModelToolsService.js'; import { IChatModel } from '../../../../common/model/chatModel.js'; import { ChatConfiguration, GeneralPurposeAgentName } from '../../../../common/constants.js'; -import { NullWorkbenchAssignmentService } from '../../../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IWorkbenchAssignmentService } from '../../../../../../services/assignment/common/assignmentService.js'; -import { Event } from '../../../../../../../base/common/event.js'; suite('RunSubagentTool', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -77,7 +74,6 @@ suite('RunSubagentTool', () => { promptsService, {} as IInstantiationService, {} as IProductService, - new NullWorkbenchAssignmentService(), )); const result = await tool.prepareToolInvocation( @@ -110,15 +106,6 @@ suite('RunSubagentTool', () => { if (opts?.customAgents) { promptsService.setCustomModes(opts.customAgents); } - const assignmentService: IWorkbenchAssignmentService = { - _serviceBrand: undefined, - onDidRefetchAssignments: Event.None, - async getCurrentExperiments() { return []; }, - async getTreatment(name: string): Promise { - return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; - }, - addTelemetryAssignmentFilter() { }, - }; const tool = testDisposables.add(new RunSubagentTool( {} as IChatAgentService, @@ -126,19 +113,16 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - new TestConfigurationService(), + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), promptsService, {} as IInstantiationService, {} as IProductService, - assignmentService, )); return tool; } async function createToolWithGPReady(opts?: { customAgents?: ICustomAgent[] }) { - const tool = createToolWithGP(opts); - await Event.toPromise(tool.onDidUpdateToolData); - return tool; + return createToolWithGP(opts); } test('treats undefined agentName as General Purpose when experiment is enabled', async () => { @@ -245,7 +229,6 @@ suite('RunSubagentTool', () => { promptsService, {} as IInstantiationService, {} as IProductService, - new NullWorkbenchAssignmentService(), )); const toolData = tool.getToolData(); @@ -261,15 +244,6 @@ suite('RunSubagentTool', () => { test('marks agentName as required when GP experiment is enabled', async () => { const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); const promptsService = new MockPromptsService(); - const assignmentService: IWorkbenchAssignmentService = { - _serviceBrand: undefined, - onDidRefetchAssignments: Event.None, - async getCurrentExperiments() { return []; }, - async getTreatment(name: string): Promise { - return (name === 'chat.generalPurposeAgent' ? true : undefined) as T | undefined; - }, - addTelemetryAssignmentFilter() { }, - }; const tool = testDisposables.add(new RunSubagentTool( {} as IChatAgentService, @@ -277,16 +251,12 @@ suite('RunSubagentTool', () => { mockToolsService, {} as ILanguageModelsService, new NullLogService(), - new TestConfigurationService(), + new TestConfigurationService({ [ChatConfiguration.GeneralPurposeAgentEnabled]: true }), promptsService, {} as IInstantiationService, {} as IProductService, - assignmentService, )); - // Wait for the constructor's async experiment resolution via onDidUpdateToolData - await Event.toPromise(tool.onDidUpdateToolData); - const toolData = tool.getToolData(); assert.ok(toolData.inputSchema?.properties?.agentName); assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description', 'agentName']); @@ -404,7 +374,6 @@ suite('RunSubagentTool', () => { promptsService, {} as IInstantiationService, {} as IProductService, - new NullWorkbenchAssignmentService(), )); return tool; @@ -681,7 +650,6 @@ suite('RunSubagentTool', () => { promptsService, mockInstantiationService as IInstantiationService, {} as IProductService, - new NullWorkbenchAssignmentService(), )); return { tool, mockChatAgentService }; From 1eb17014f8ce8d2281378928cc4775c55f225786 Mon Sep 17 00:00:00 2001 From: digitarald Date: Thu, 2 Apr 2026 00:01:50 -0400 Subject: [PATCH 8/9] Mark GP agent setting as advanced --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- .../chat/common/promptSyntax/computeAutomaticInstructions.ts | 1 - .../common/promptSyntax/computeAutomaticInstructions.test.ts | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b800c491268f2..690322fdd1170 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1363,7 +1363,7 @@ configurationRegistry.registerConfiguration({ 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'], + tags: ['experimental', 'advanced'], experiment: { mode: 'auto' } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 3df5a2a1bc77f..1b6fed663b4c6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -458,7 +458,6 @@ export class ComputeAutomaticInstructions { 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)) { 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 eef59d432eb07..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 @@ -1537,7 +1537,6 @@ suite('ComputeAutomaticInstructions', () => { ChatModeKind.Agent, { 'vscode_runSubagent': true }, ['*'], - undefined ); const variables = new ChatRequestVariableSet(); @@ -1571,7 +1570,6 @@ suite('ComputeAutomaticInstructions', () => { ChatModeKind.Agent, { 'vscode_runSubagent': true }, ['*'], - undefined ); const variables = new ChatRequestVariableSet(); From cefcd2d85fab4381411d0b0f2b687149d2621326 Mon Sep 17 00:00:00 2001 From: digitarald Date: Thu, 2 Apr 2026 01:06:08 -0400 Subject: [PATCH 9/9] Address PR review: gate agentName schema, normalize GP name, improve error hints --- .../tools/builtinTools/runSubagentTool.ts | 72 +++++++++++-------- .../builtinTools/runSubagentTool.test.ts | 4 +- 2 files changed, 45 insertions(+), 31 deletions(-) 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 abecd3b1235ca..f953732e3414f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -95,27 +95,38 @@ export class RunSubagentTool extends Disposable implements IToolImpl { getToolData(): IToolData { 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' + }, + description: { + type: 'string', + description: 'A short (3-5 word) description of the task' + } + }; + + if (customAgentsEnabled || generalPurposeAgentEnabled) { + properties.agentName = { + type: 'string', + description: generalPurposeAgentEnabled + ? 'Name of the agent to invoke.' + : 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' + }; + } + + const required: string[] = ['prompt', 'description']; + if (generalPurposeAgentEnabled) { + required.push('agentName'); + } + 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' - }, - agentName: { - type: 'string', - description: this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) - ? 'Name of the agent to invoke.' - : 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' - } - }, - required: this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) - ? ['prompt', 'description', 'agentName'] - : ['prompt', 'description'] + properties, + required }; const runSubagentToolData: IToolData = { id: RunSubagentTool.Id, @@ -165,10 +176,13 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const subAgentName = args.agentName; // Defensive: model may omit agentName despite schema requiring it - const isGeneralPurpose = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) && (!subAgentName || subAgentName === GeneralPurposeAgentName); + 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 = await this.getSubAgentByName(subAgentName); + subagent = customAgentsEnabled ? await this.getSubAgentByName(subAgentName) : undefined; if (subagent) { // Check the pre-resolved model cache from prepareToolInvocation const cached = this._resolvedModels.get(invocation.callId); @@ -207,11 +221,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { }; } else { this._resolvedModels.delete(invocation.callId); - const gpEnabled = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled); - const hint = gpEnabled - ? ` Use '${GeneralPurposeAgentName}' for a full-capability agent.` - : ''; - throw new Error(`Requested agent '${subAgentName}' not found.${hint}`); + 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 @@ -323,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, @@ -445,8 +457,10 @@ export class RunSubagentTool extends Disposable implements IToolImpl { const args = context.parameters as IRunSubagentToolInputParams; // Defensive: model may omit agentName despite schema requiring it - const isGeneralPurpose = this.configurationService.getValue(ChatConfiguration.GeneralPurposeAgentEnabled) && (!args.agentName || args.agentName === GeneralPurposeAgentName); - const subagent = (args.agentName && !isGeneralPurpose) ? await this.getSubAgentByName(args.agentName) : undefined; + 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); 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 6a101be5f247a..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 @@ -237,7 +237,7 @@ suite('RunSubagentTool', () => { assert.ok(toolData.inputSchema); assert.ok(toolData.inputSchema.properties?.prompt); assert.ok(toolData.inputSchema.properties?.description); - assert.ok(toolData.inputSchema.properties?.agentName); + 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']); }); @@ -370,7 +370,7 @@ suite('RunSubagentTool', () => { mockToolsService, mockLanguageModelsService as ILanguageModelsService, new NullLogService(), - new TestConfigurationService(), + new TestConfigurationService({ [ChatConfiguration.SubagentToolCustomAgents]: true }), promptsService, {} as IInstantiationService, {} as IProductService,