Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,15 +1217,6 @@ configurationRegistry.registerConfiguration({
mode: 'auto'
}
},
[ChatConfiguration.SubagentToolCustomAgents]: {
type: 'boolean',
description: nls.localize('chat.subagentTool.customAgents', "Whether the runSubagent tool is able to use custom agents. When enabled, the tool can take the name of a custom agent, but it must be given the exact name of the agent."),
default: false,
tags: ['experimental'],
experiment: {
mode: 'auto'
}
},
[ChatConfiguration.ChatCustomizationMenuEnabled]: {
type: 'boolean',
description: nls.localize('chat.aiCustomizationMenu.enabled', "Controls whether the Chat Customization Menu is shown in the Manage menu and Command Palette. When disabled, the Chat Customizations editor and related commands are hidden."),
Expand Down
7 changes: 6 additions & 1 deletion src/vs/workbench/contrib/chat/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export enum ChatConfiguration {
ChatViewSessionsOrientation = 'chat.viewSessions.orientation',
ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled',
ChatContextUsageEnabled = 'chat.contextUsage.enabled',
SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled',
ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress',
RestoreLastPanelSession = 'chat.restoreLastPanelSession',
ExitAfterDelegation = 'chat.exitAfterDelegation',
Expand Down Expand Up @@ -171,3 +170,9 @@ export const ChatEditorTitleMaxLength = 30;
export const CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES = 1000;
export const CONTEXT_MODELS_EDITOR = new RawContextKey<boolean>('inModelsEditor', false);
export const CONTEXT_MODELS_SEARCH_FOCUS = new RawContextKey<boolean>('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';
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { PromptsType } from './promptTypes.js';
import { ParsedPromptFile } from './promptFileParser.js';
import { AgentFileType, ICustomAgent, IPromptPath, IPromptsService } from './service/promptsService.js';
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
import { ChatConfiguration, ChatModeKind } from '../constants.js';
import { ChatModeKind, GeneralPurposeAgentName } from '../constants.js';
import { UserSelectedTools } from '../participants/chatAgents.js';

export type InstructionsCollectionEvent = {
Expand Down Expand Up @@ -391,7 +391,18 @@ export class ComputeAutomaticInstructions {
entries.push('</skills>', '', ''); // add trailing newline
}
}
if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) {
if (runSubagentTool) {
entries.push('<agents>');
entries.push('Here is a list of agents that can be used when running a subagent.');
entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.');
entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`);

// Built-in General Purpose agent, always available
entries.push('<agent>');
entries.push(`<name>${GeneralPurposeAgentName}</name>`);
entries.push(`<description>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.</description>`);
entries.push('</agent>');

const canUseAgent = (() => {
if (!this._enabledSubagents || this._enabledSubagents.includes('*')) {
return (agent: ICustomAgent) => agent.visibility.agentInvocable;
Expand All @@ -401,29 +412,23 @@ export class ComputeAutomaticInstructions {
}
})();
const agents = await this._promptsService.getCustomAgents(token);
if (agents.length > 0) {
entries.push('<agents>');
entries.push('Here is a list of agents that can be used when running a subagent.');
entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.');
entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`);
for (const agent of agents) {
if (canUseAgent(agent)) {
entries.push('<agent>');
entries.push(`<name>${agent.name}</name>`);
if (agent.description) {
entries.push(`<description>${agent.description}</description>`);
}
if (agent.argumentHint) {
entries.push(`<argumentHint>${agent.argumentHint}</argumentHint>`);
}
entries.push('</agent>');
if (isInClaudeAgentsFolder(agent.uri)) {
telemetryEvent.claudeAgentsCount++;
}
for (const agent of agents) {
if (canUseAgent(agent)) {
entries.push('<agent>');
entries.push(`<name>${agent.name}</name>`);
if (agent.description) {
entries.push(`<description>${agent.description}</description>`);
}
if (agent.argumentHint) {
entries.push(`<argumentHint>${agent.argumentHint}</argumentHint>`);
}
entries.push('</agent>');
if (isInClaudeAgentsFolder(agent.uri)) {
telemetryEvent.claudeAgentsCount++;
}
}
entries.push('</agents>', '', ''); // add trailing newline
}
entries.push('</agents>', '', ''); // add trailing newline
}
if (entries.length === 0) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ 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 { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js';
import { IChatProgress, IChatService } from '../../chatService/chatService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js';
import { ChatAgentLocation, ChatModeKind, GeneralPurposeAgentName } from '../../constants.js';
import { ILanguageModelsService } from '../../languageModels.js';
import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js';
import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js';
Expand Down Expand Up @@ -47,19 +46,20 @@ 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;
description: string;
agentName?: string;
agentName: string;
Comment thread
digitarald marked this conversation as resolved.
}

export class RunSubagentTool extends Disposable implements IToolImpl {

static readonly Id = 'runSubagent';

readonly onDidUpdateToolData: Event<IConfigurationChangeEvent>;
readonly onDidUpdateToolData: Event<void>;

/** Hack to port data between prepare/invoke */
private readonly _resolvedModels = new Map<string, { modeModelId: string | undefined; resolvedModelName: string | undefined }>();
Expand All @@ -70,17 +70,15 @@ 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,
) {
super();
this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents));
Comment thread
digitarald marked this conversation as resolved.
this.onDidUpdateToolData = Event.None;
}

getToolData(): IToolData {
let modelDescription = BaseModelDescription;
const modelDescription = BaseModelDescription;
const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = {
type: 'object',
properties: {
Expand All @@ -91,18 +89,14 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
description: {
type: 'string',
description: 'A short (3-5 word) description of the task'
},
agentName: {
type: 'string',
description: 'Name of the agent to invoke.'
}
},
required: ['prompt', 'description']
required: ['prompt', 'description', 'agentName']
};

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,
Expand Down Expand Up @@ -150,7 +144,11 @@ 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 = !subAgentName || subAgentName === GeneralPurposeAgentName;

if (!isGeneralPurpose) {
// Custom agent; look up by name and apply its model/tools/instructions
subagent = await this.getSubAgentByName(subAgentName);
if (subagent) {
// Check the pre-resolved model cache from prepareToolInvocation
Expand Down Expand Up @@ -184,14 +182,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,
};
} 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
// General Purpose agent; inherit parent's model and tools
const cached = this._resolvedModels.get(invocation.callId);
if (cached) {
this._resolvedModels.delete(invocation.callId);
Expand Down Expand Up @@ -374,7 +373,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
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 = !args.agentName || args.agentName === GeneralPurposeAgentName;
const subagent = isGeneralPurpose ? undefined : await this.getSubAgentByName(args.agentName);

// Resolve the model early and cache it for invoke()
const resolved = this.resolveSubagentModel(subagent, context.modelId);
Expand All @@ -385,7 +386,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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,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 { IAgentPluginService } from '../../../common/plugins/agentPluginService.js';
Expand Down Expand Up @@ -1036,7 +1036,6 @@ suite('ComputeAutomaticInstructions', () => {
const rootFolderUri = URI.file(rootFolder);

workspaceContextService.setWorkspace(testWorkspace(rootFolderUri));
testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true);
testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, {
[AGENTS_SOURCE_FOLDER]: true,
'.claude/agents': true,
Expand Down Expand Up @@ -1159,9 +1158,6 @@ suite('ComputeAutomaticInstructions', () => {

workspaceContextService.setWorkspace(testWorkspace(rootFolderUri));

// Enable the config for custom agents
testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true);

await mockFiles(fileService, [
{
path: `${rootFolder}/.github/agents/test-agent-1.agent.md`,
Expand Down Expand Up @@ -1235,16 +1231,19 @@ suite('ComputeAutomaticInstructions', () => {
assert.equal(agentsList.length, 1, 'There should be one agents list');

const agents = xmlContents(agentsList[0], 'agent');
assert.equal(agents.length, 3, 'There should be three agents');
assert.equal(agents.length, 4, 'There should be four agents (General Purpose + 3 custom)');

// First agent should always be the built-in General Purpose agent
assert.equal(xmlContents(agents[0], 'name')[0], GeneralPurposeAgentName);

assert.equal(xmlContents(agents[0], 'description')[0], 'Test agent 1');
assert.equal(xmlContents(agents[0], 'name')[0], `test-agent-1`);
assert.equal(xmlContents(agents[1], 'description')[0], 'Test agent 1');
assert.equal(xmlContents(agents[1], 'name')[0], `test-agent-1`);

assert.equal(xmlContents(agents[1], 'description')[0], 'Test agent 3');
assert.equal(xmlContents(agents[1], 'name')[0], `test-agent-3`);
assert.equal(xmlContents(agents[2], 'description')[0], 'Test agent 3');
assert.equal(xmlContents(agents[2], 'name')[0], `test-agent-3`);

assert.equal(xmlContents(agents[2], 'description')[0], 'Test agent 5');
assert.equal(xmlContents(agents[2], 'name')[0], `test-agent-5`);
assert.equal(xmlContents(agents[3], 'description')[0], 'Test agent 5');
assert.equal(xmlContents(agents[3], 'name')[0], `test-agent-5`);
});

test('should include skills list when readFile tool available', async () => {
Expand Down
Loading
Loading