diff --git a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts index 9d675b67529a95..cb225cac8bdfcb 100644 --- a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts @@ -44,14 +44,40 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque const lookForTags = new Set(['vscode_codesearch']); const endpointProvider = accessor.get(IEndpointProvider); const model = await endpointProvider.getChatEndpoint(request); - - // Special case... - // Since AskAgent currently has no tool picker, have to duplicate the toolReference logic here. - // When it's no longer experimental, it should be a custom mode, have a tool picker, etc. - // And must return boolean to avoid falling back on other logic that we don't want, like the `extension_installed_by_tool` check. - return toolsService.getEnabledTools(request, model, tool => tool.tags.some(tag => lookForTags.has(tag)) || request.toolReferences.some(ref => ref.name === tool.name)); + return toolsService.getEnabledTools(request, model, tool => askAgentToolFilter(tool, request)); }); +/** + * Filters tools for the ask agent. Since AskAgent currently has no tool picker, + * we duplicate the toolReference logic here. When it's no longer experimental, + * it should be a custom mode, have a tool picker, etc. + * + * MCP tools are registered dynamically as the MCP server connects and discovers + * its tools. The `toolReferences` in the request are a snapshot taken when the + * request was parsed, which can happen before the MCP server finished + * discovering all of its tools. When the user referenced an MCP server (any of + * its tools appear in `toolReferences`), include all currently registered MCP + * tools so that availability reflects the live registration state rather than + * the possibly-stale snapshot. See #334569. + * + * Must return boolean to avoid falling back on other logic that we don't want, + * like the `extension_installed_by_tool` check. + */ +export function askAgentToolFilter(tool: vscode.LanguageModelToolInformation, request: vscode.ChatRequest): boolean { + const lookForTags = new Set(['vscode_codesearch']); + if (tool.tags.some(tag => lookForTags.has(tag)) || request.toolReferences.some(ref => ref.name === tool.name)) { + return true; + } + if (tool.tags.includes('mcp')) { + // The user referenced an MCP server. MCP tools are registered with the + // `mcp_` prefix (see McpToolName.Prefix), so check the reference names + // rather than looking up the tool, which may not be registered yet when + // the request snapshot was taken. + return request.toolReferences.some(ref => ref.name.startsWith('mcp_')); + } + return false; +} + export class AskAgentIntent implements IIntent { static readonly ID = Intent.AskAgent; diff --git a/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts b/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts new file mode 100644 index 00000000000000..a3a58776d53178 --- /dev/null +++ b/extensions/copilot/src/extension/intents/test/node/askAgentIntent.spec.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import type { ChatRequest, LanguageModelToolInformation } from 'vscode'; +import { askAgentToolFilter } from '../../node/askAgentIntent'; + +function makeTool(name: string, tags: string[] = []): LanguageModelToolInformation { + return { + name, + description: `description for ${name}`, + inputSchema: { type: 'object' }, + tags, + source: undefined, + }; +} + +function makeRequest(toolReferences: { name: string }[]): ChatRequest { + return { + requestId: 'request-id', + message: 'test', + agentId: 'copilot-chat', + prompt: 'test', + references: [], + model: undefined, + toolReferences, + tools: [], + history: [], + }; +} + +describe('askAgentToolFilter', () => { + it('includes tools with the codesearch tag', () => { + const tool = makeTool('vscode_search', ['vscode_codesearch']); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('includes tools referenced in the request', () => { + const tool = makeTool('vscode_read_file', []); + const request = makeRequest([{ name: 'vscode_read_file' }]); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('excludes unreferenced non-MCP tools', () => { + const tool = makeTool('vscode_edit_file', []); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); + + it('includes MCP tools when the user referenced an MCP server tool', () => { + // The request snapshot was taken while the MCP server was still + // discovering tools, so it only contains one of the server's tools. + const request = makeRequest([{ name: 'mcp_fire_take_snapshot' }]); + // Tools registered *after* the snapshot are now available too. + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + expect(askAgentToolFilter(tool, request)).toBe(true); + }); + + it('excludes MCP tools when the user did not reference an MCP server', () => { + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + const request = makeRequest([{ name: 'vscode_read_file' }]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); + + it('excludes MCP tools when the request has no tool references at all', () => { + const tool = makeTool('mcp_fire_navigate_page', ['mcp']); + const request = makeRequest([]); + expect(askAgentToolFilter(tool, request)).toBe(false); + }); +});