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
38 changes: 32 additions & 6 deletions extensions/copilot/src/extension/intents/node/askAgentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,40 @@ const getTools = (instaService: IInstantiationService, request: vscode.ChatReque
const lookForTags = new Set<string>(['vscode_codesearch']);
const endpointProvider = accessor.get<IEndpointProvider>(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));
Comment thread
luotianyiismywife marked this conversation as resolved.
});

/**
* 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<string>(['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_'));
Comment thread
luotianyiismywife marked this conversation as resolved.
}
return false;
}

export class AskAgentIntent implements IIntent {

static readonly ID = Intent.AskAgent;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});