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
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode.
allowTools[ToolName.GetScmChanges] = getSCMChangesEnabled;

allowTools[ToolName.SessionStoreSql] = true;
allowTools[ToolName.CoreAskQuestions] = true;

allowTools[CUSTOM_TOOL_SEARCH_NAME] = !!model.supportsToolSearch;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio
import { createExtensionUnitTestingServices } from '../../../test/node/services';
import { TestChatRequest } from '../../../test/node/testHelpers';
import { ToolName } from '../../../tools/common/toolNames';
import { IToolsService } from '../../../tools/common/toolsService';
import { TestToolsService } from '../../../tools/node/test/testToolsService';
import { AgentIntentInvocation, getAgentTools, isBackgroundTodoAgentEnabled, isTodoToolExplicitlyEnabled } from '../agentIntent';

// ─── isTodoToolExplicitlyEnabled unit tests ──────────────────────
Expand Down Expand Up @@ -86,6 +88,13 @@ describe('getAgentTools background todo enablement', () => {
instantiationService = accessor.get(IInstantiationService);
configService = accessor.get(IConfigurationService);
experimentationService = accessor.get(IExperimentationService);
(accessor.get(IToolsService) as TestToolsService).addTestToolOverride({
name: ToolName.CoreAskQuestions,
description: 'ask questions',
inputSchema: { type: 'object', properties: {} },
tags: [],
source: undefined,
}, {} as any);
mockEndpoint = instantiationService.createInstance(MockEndpoint, undefined);
});

Expand All @@ -102,6 +111,10 @@ describe('getAgentTools background todo enablement', () => {
return tools.some(t => t.name === ToolName.CoreManageTodoList);
}

function hasTool(tools: readonly { name: string }[], name: string): boolean {
return tools.some(tool => tool.name === name);
}

test('background todo agent is enabled only when experiment is on and todo is not explicit', () => {
const request = new TestChatRequest('fix the bug');
configService.setConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, false);
Expand Down Expand Up @@ -137,6 +150,19 @@ describe('getAgentTools background todo enablement', () => {
const tools = await instantiationService.invokeFunction(getAgentTools, request, mockEndpoint);
expect(hasTodoTool(tools)).toBe(false);
});

test('publishes vscode_askQuestions on the initial agent request without explicit tool picker opt-in', async () => {
const request = new TestChatRequest('fix the bug');
const tools = await instantiationService.invokeFunction(getAgentTools, request, mockEndpoint);
expect(hasTool(tools, ToolName.CoreAskQuestions)).toBe(true);
});

test('does not publish vscode_askQuestions when the tool picker explicitly disables it', async () => {
const request = new TestChatRequest('fix the bug');
request.tools = new Map([[{ name: ToolName.CoreAskQuestions } as any, false]]);
const tools = await instantiationService.invokeFunction(getAgentTools, request, mockEndpoint);
expect(hasTool(tools, ToolName.CoreAskQuestions)).toBe(false);
});
});

// ─── _maybeStartBackgroundTodoPass subagent guard ────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { IConversationOptions } from '../../../platform/chat/common/conversation
import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession, NullEditSurvivalTrackingSession } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';
import { isAnthropicFamily } from '../../../platform/endpoint/common/chatModelCapabilities';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { IGitService } from '../../../platform/git/common/gitService';
Expand Down Expand Up @@ -772,11 +771,6 @@ class DefaultToolCallingLoop extends ToolCallingLoop<IDefaultToolLoopOptions> {
protected override async getAvailableTools(outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise<LanguageModelToolInformation[]> {
const tools = await this.options.invocation.getAvailableTools?.() ?? [];

// Skip tool grouping when Anthropic tool search is enabled
if (isAnthropicFamily(this.options.invocation.endpoint) && this.options.invocation.endpoint.supportsToolSearch) {
return tools;
}

if (this.toolGrouping) {
this.toolGrouping.tools = tools;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { Raw, RenderPromptResult } from '@vscode/prompt-tsx';
import { afterEach, beforeEach, expect, suite, test, vi } from 'vitest';
import type { ChatLanguageModelToolReference, ChatPromptReference, ChatRequest, ExtendedChatResponsePart, LanguageModelChat } from 'vscode';
import type { ChatLanguageModelToolReference, ChatPromptReference, ChatRequest, ExtendedChatResponsePart, LanguageModelChat, LanguageModelToolInformation } from 'vscode';
import { IChatMLFetcher } from '../../../../platform/chat/common/chatMLFetcher';
import { toTextPart } from '../../../../platform/chat/common/globalStringUtils';
import { StaticChatMLFetcher } from '../../../../platform/chat/test/common/staticChatMLFetcher';
Expand All @@ -20,6 +20,7 @@ import { NullWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearc
import { IWorkspaceFileIndex } from '../../../../platform/workspaceChunkSearch/node/workspaceFileIndex';
import { ChatResponseStreamImpl } from '../../../../util/common/chatResponseStreamImpl';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import { constObservable } from '../../../../util/vs/base/common/observableInternal';
import { isObject, isUndefinedOrNull } from '../../../../util/vs/base/common/types';
import { generateUuid } from '../../../../util/vs/base/common/uuid';
import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';
Expand All @@ -28,6 +29,7 @@ import { ChatLocation, ChatResponseConfirmationPart, ChatResponseMarkdownPart, L
import { ToolCallingLoop } from '../../../intents/node/toolCallingLoop';
import { ToolResultMetadata } from '../../../prompts/node/panel/toolCalling';
import { createExtensionUnitTestingServices } from '../../../test/node/services';
import { IToolGrouping, IToolGroupingService } from '../../../tools/common/virtualTools/virtualToolTypes';
import { Conversation, Turn } from '../../common/conversation';
import { IBuildPromptContext } from '../../common/intents';
import { ToolCallRound } from '../../common/toolCallRound';
Expand All @@ -42,9 +44,12 @@ suite('defaultIntentRequestHandler', () => {
let promptResult: RenderPromptResult | RenderPromptResult[];
let telemetry: SpyingTelemetryService;
let fetcher: StaticChatMLFetcher;
let endpoint: IChatEndpoint;
let endpoint: MockEndpoint;
let turnIdCounter = 0;
let builtPrompts: IBuildPromptContext[] = [];
let availableTools: LanguageModelToolInformation[] = [];
let groupedTools: LanguageModelToolInformation[] | undefined;
let toolGroupingComputeCount = 0;
const sessionId = 'some-session-id';

const getTurnId = () => `turn-id-${turnIdCounter}`;
Expand All @@ -57,12 +62,22 @@ suite('defaultIntentRequestHandler', () => {
services.define(ITelemetryService, telemetry);
services.define(IChatMLFetcher, fetcher);
services.define(IWorkspaceFileIndex, new SyncDescriptor(NullWorkspaceFileIndex));
services.define(IToolGroupingService, {
_serviceBrand: undefined,
threshold: constObservable(0),
create: (_sessionId, tools) => new TestToolGrouping(tools, () => groupedTools ?? tools, () => {
toolGroupingComputeCount++;
}),
});

accessor = services.createTestingAccessor();
endpoint = accessor.get(IInstantiationService).createInstance(MockEndpoint, undefined);
builtPrompts = [];
response = [];
promptResult = nullRenderPromptResult();
availableTools = [];
groupedTools = undefined;
toolGroupingComputeCount = 0;
turnIdCounter = 0;
(ToolCallingLoop as any).NextToolCallId = 0;
(ToolCallRound as any).generateID = () => 'static-id';
Expand All @@ -87,12 +102,65 @@ suite('defaultIntentRequestHandler', () => {
});
}

class TestToolGrouping implements IToolGrouping {
public tools: readonly LanguageModelToolInformation[];

constructor(
tools: readonly LanguageModelToolInformation[],
private readonly getComputedTools: () => readonly LanguageModelToolInformation[],
private readonly onCompute: () => void,
) {
this.tools = tools;
}

didCall(): LanguageModelToolResult | undefined {
return undefined;
}

didTakeTurn(): void {
}

didInvalidateCache(): void {
}

getContainerFor() {
return undefined;
}

ensureExpanded(): void {
}

async compute(): Promise<LanguageModelToolInformation[]> {
this.onCompute();
return [...this.getComputedTools()];
}

async computeAll(): Promise<LanguageModelToolInformation[]> {
this.onCompute();
return [...this.getComputedTools()];
}
}

function makeToolInfo(name: string): LanguageModelToolInformation {
return {
name,
description: `Tool for ${name}`,
inputSchema: { type: 'object', properties: {} },
source: undefined,
tags: [],
};
}

class TestIntent implements IIntent {
id = 'test';
description = 'test intent';
locations = [ChatLocation.Panel];

constructor(private readonly tools: readonly LanguageModelToolInformation[] = []) {
}

invoke(): Promise<IIntentInvocation> {
return Promise.resolve(new TestIntentInvocation(this, this.locations[0], endpoint));
return Promise.resolve(new TestIntentInvocation(this, this.locations[0], endpoint, this.tools));
}
}

Expand All @@ -103,8 +171,13 @@ suite('defaultIntentRequestHandler', () => {
readonly intent: IIntent,
readonly location: ChatLocation,
readonly endpoint: IChatEndpoint,
private readonly tools: readonly LanguageModelToolInformation[],
) { }

getAvailableTools(): LanguageModelToolInformation[] {
return [...this.tools];
}

async buildPrompt(context: IBuildPromptContext): Promise<RenderPromptResult> {
builtPrompts.push(context);
if (Array.isArray(promptResult)) {
Expand Down Expand Up @@ -145,8 +218,9 @@ suite('defaultIntentRequestHandler', () => {

const makeHandler = ({
request = new TestChatRequest(),
turns = []
}: { request?: ChatRequest; turns?: Turn[] } = {}) => {
turns = [],
tools = availableTools,
}: { request?: ChatRequest; turns?: Turn[]; tools?: readonly LanguageModelToolInformation[] } = {}) => {
turns.push(new Turn(
getTurnId(),
{ type: 'user', message: request.prompt },
Expand All @@ -156,7 +230,7 @@ suite('defaultIntentRequestHandler', () => {
const instaService = accessor.get(IInstantiationService);
return instaService.createInstance(
DefaultIntentRequestHandler,
new TestIntent(),
new TestIntent(tools),
new Conversation(sessionId, turns),
request,
responseStream,
Expand Down Expand Up @@ -191,6 +265,34 @@ suite('defaultIntentRequestHandler', () => {
expect(getDerandomizedTelemetry()).toMatchSnapshot();
});

test('keeps activate_vs_code_interaction in request-scoped tools for anthropic tool search requests', async () => {
endpoint.family = 'claude-sonnet-4';
endpoint.model = 'claude-sonnet-4';
endpoint.supportsToolSearch = true;

availableTools = [
makeToolInfo('read_file'),
makeToolInfo('get_errors'),
makeToolInfo('create_new_workspace'),
];
groupedTools = [
makeToolInfo('read_file'),
makeToolInfo('activate_vs_code_interaction'),
];

const handler = makeHandler({ tools: availableTools });
chatResponse[0] = 'some response here :)';
promptResult = {
...nullRenderPromptResult(),
messages: [{ role: Raw.ChatRole.User, content: [toTextPart('hello world!')] }],
};

await handler.getResult();

expect(toolGroupingComputeCount).toBeGreaterThan(0);
expect(builtPrompts[0].tools?.availableTools.map(tool => tool.name)).toContain('activate_vs_code_interaction');
});

test('propagates resolvedModel into result metadata from a successful response', async () => {
fetcher.resolvedModel = 'gpt-4o-resolved';
const handler = makeHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ export class AgentUserMessage extends PromptElement<AgentUserMessageProps> {
const ReminderInstructionsClass = this.props.ReminderInstructionsClass ?? DefaultReminderInstructions;
const reminderProps: ReminderInstructionsProps = {
endpoint: this.props.endpoint,
availableTools: this.props.availableTools,
hasTodoTool,
hasEditFileTool,
hasReplaceStringTool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class DefaultToolReferencesHint extends PromptElement<ToolReferencesHintP

export interface ReminderInstructionsProps extends BasePromptElementProps {
readonly endpoint: IChatEndpoint;
readonly availableTools: readonly LanguageModelToolInformation[] | undefined;
readonly hasTodoTool: boolean;
readonly hasEditFileTool: boolean;
readonly hasReplaceStringTool: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { PromptElement, PromptSizing } from '@vscode/prompt-tsx';
import { PromptElement, PromptElementProps, PromptSizing } from '@vscode/prompt-tsx';
import { isGpt54 } from '../../../../../platform/endpoint/common/chatModelCapabilities';
import { IChatEndpoint } from '../../../../../platform/networking/common/networking';
import { IToolDeferralService } from '../../../../../platform/networking/common/toolDeferralService';
import { ToolName } from '../../../../tools/common/toolNames';
import { GPT5CopilotIdentityRule } from '../../base/copilotIdentity';
import { InstructionMessage } from '../../base/instructionMessage';
Expand All @@ -16,7 +17,7 @@ import { ResponseRenderingRules } from '../../panel/editorIntegrationRules';
import { ApplyPatchInstructions, DefaultAgentPromptProps, detectToolCapabilities, getEditingReminder, McpToolInstructions, ReminderInstructionsProps } from '../defaultAgentInstructions';
import { FileLinkificationInstructions } from '../fileLinkificationInstructions';
import { CopilotIdentityRulesConstructor, IAgentPrompt, PromptRegistry, ReminderInstructionsConstructor, SafetyRulesConstructor, SystemPrompt } from '../promptRegistry';
import { CUSTOM_TOOL_SEARCH_NAME, ToolSearchToolPromptOptimized } from '../toolSearchInstructions';
import { CUSTOM_TOOL_SEARCH_NAME, shouldShowToolSearchPrompt, ToolSearchToolPromptOptimized } from '../toolSearchInstructions';

export class Gpt54Prompt extends PromptElement<DefaultAgentPromptProps> {
async render(state: void, sizing: PromptSizing) {
Expand Down Expand Up @@ -241,8 +242,15 @@ class Gpt54PromptResolver implements IAgentPrompt {
}

export class Gpt54ReminderInstructions extends PromptElement<ReminderInstructionsProps> {
constructor(
props: PromptElementProps<ReminderInstructionsProps>,
@IToolDeferralService private readonly toolDeferralService: IToolDeferralService,
) {
super(props);
}

async render(state: void, sizing: PromptSizing) {
const toolSearchEnabled = !!this.props.endpoint.supportsToolSearch;
const toolSearchEnabled = shouldShowToolSearchPrompt(this.props.availableTools, this.props.endpoint.supportsToolSearch, this.toolDeferralService);
return <>
You are an agent—keep going until the user's query is completely resolved before ending your turn. ONLY stop if solved or genuinely blocked.<br />
Take action when possible; the user expects you to do useful work without unnecessary questions.<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { PromptElement, PromptSizing } from '@vscode/prompt-tsx';
import { PromptElement, PromptElementProps, PromptSizing } from '@vscode/prompt-tsx';
import { IToolDeferralService } from '../../../../../platform/networking/common/toolDeferralService';
import { ToolName } from '../../../../tools/common/toolNames';
import { InstructionMessage } from '../../base/instructionMessage';
import { ResponseTranslationRules } from '../../base/responseTranslationRules';
import { Tag } from '../../base/tag';
import { ResponseRenderingRules } from '../../panel/editorIntegrationRules';
import { ApplyPatchInstructions, DefaultAgentPromptProps, detectToolCapabilities, getEditingReminder, McpToolInstructions, ReminderInstructionsProps } from '../defaultAgentInstructions';
import { FileLinkificationInstructionsOptimized } from '../fileLinkificationInstructions';
import { CUSTOM_TOOL_SEARCH_NAME, ToolSearchToolPromptOptimized } from '../toolSearchInstructions';
import { CUSTOM_TOOL_SEARCH_NAME, shouldShowToolSearchPrompt, ToolSearchToolPromptOptimized } from '../toolSearchInstructions';

export abstract class Gpt55PromptBase extends PromptElement<DefaultAgentPromptProps> {
protected get includeLargePromptSections(): boolean {
Expand Down Expand Up @@ -266,8 +267,15 @@ export abstract class Gpt55PromptBase extends PromptElement<DefaultAgentPromptPr
}

export class Gpt55ReminderInstructions extends PromptElement<ReminderInstructionsProps> {
constructor(
props: PromptElementProps<ReminderInstructionsProps>,
@IToolDeferralService private readonly toolDeferralService: IToolDeferralService,
) {
super(props);
}

async render(state: void, sizing: PromptSizing) {
const toolSearchEnabled = !!this.props.endpoint.supportsToolSearch;
const toolSearchEnabled = shouldShowToolSearchPrompt(this.props.availableTools, this.props.endpoint.supportsToolSearch, this.toolDeferralService);
return <>
You are an agent—keep going until the user's query is completely resolved before ending your turn. ONLY stop if solved or genuinely blocked.<br />
Take action when possible; the user expects you to do useful work without unnecessary questions.<br />
Expand Down
Loading