Path: blob/main/extensions/copilot/src/extension/prompt/node/searchSubagentToolCallingLoop.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { randomUUID } from 'crypto';6import type { CancellationToken, ChatRequest, ChatResponseStream, LanguageModelToolInformation, Progress } from 'vscode';7import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';8import { IChatHookService } from '../../../platform/chat/common/chatHookService';9import { ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes';10import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService';11import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';12import { ChatEndpointFamily, IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';13import { ProxyAgenticEndpoint } from '../../../platform/endpoint/node/proxyAgenticEndpoint';14import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';15import { IGitService } from '../../../platform/git/common/gitService';16import { ILogService } from '../../../platform/log/common/logService';17import { IOTelService } from '../../../platform/otel/common/otelService';18import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';19import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';20import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';21import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';22import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes';23import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop';24import { SearchSubagentPrompt } from '../../prompts/node/agent/searchSubagentPrompt';25import { PromptRenderer } from '../../prompts/node/base/promptRenderer';26import { ToolName } from '../../tools/common/toolNames';27import { IToolsService } from '../../tools/common/toolsService';28import { IBuildPromptContext } from '../common/intents';29import { IBuildPromptResult } from './intents';3031export interface ISearchSubagentToolCallingLoopOptions extends IToolCallingLoopOptions {32request: ChatRequest;33location: ChatLocation;34promptText: string;35/** Optional pre-generated subagent invocation ID. If not provided, a new UUID will be generated. */36subAgentInvocationId?: string;37/** The tool_call_id from the parent agent's LLM response that triggered this subagent invocation. */38parentToolCallId?: string;39/** The headerRequestId from the parent agent's fetch response that triggered this subagent invocation. */40parentHeaderRequestId?: string;41/** Thoroughness level for the search, passed through to the prompt when thoroughnessEnabled config is on. */42thoroughness?: 'normal' | 'deep';43}4445export class SearchSubagentToolCallingLoop extends ToolCallingLoop<ISearchSubagentToolCallingLoopOptions> {4647public static readonly ID = 'searchSubagentTool';4849constructor(50options: ISearchSubagentToolCallingLoopOptions,51@IInstantiationService private readonly instantiationService: IInstantiationService,52@ILogService logService: ILogService,53@IRequestLogger requestLogger: IRequestLogger,54@IEndpointProvider private readonly endpointProvider: IEndpointProvider,55@IToolsService private readonly toolsService: IToolsService,56@IAuthenticationChatUpgradeService authenticationChatUpgradeService: IAuthenticationChatUpgradeService,57@ITelemetryService telemetryService: ITelemetryService,58@IConfigurationService configurationService: IConfigurationService,59@IExperimentationService experimentationService: IExperimentationService,60@IChatHookService chatHookService: IChatHookService,61@ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService,62@IFileSystemService fileSystemService: IFileSystemService,63@IOTelService otelService: IOTelService,64@IGitService gitService: IGitService,65) {66super(options, instantiationService, endpointProvider, logService, requestLogger, authenticationChatUpgradeService, telemetryService, configurationService, experimentationService, chatHookService, sessionTranscriptService, fileSystemService, otelService, gitService);67}6869protected override createPromptContext(availableTools: LanguageModelToolInformation[], outputStream: ChatResponseStream | undefined): IBuildPromptContext {70const context = super.createPromptContext(availableTools, outputStream);71if (context.tools) {72context.tools = {73...context.tools,74toolReferences: [],75subAgentInvocationId: this.options.subAgentInvocationId ?? randomUUID(),76subAgentName: 'search'77};78}79context.query = this.options.promptText;80return context;81}8283private static readonly DEFAULT_AGENTIC_PROXY_MODEL = 'vscode-agentic-search-router-a';8485/**86* Get the endpoint to use for the search subagent87*/88private async getEndpoint() {89const modelName = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentModel, this._experimentationService) as ChatEndpointFamily | undefined;90const useAgenticProxy = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentUseAgenticProxy, this._experimentationService);9192if (useAgenticProxy) {93// Use agentic proxy with SearchSubagentModel or default to 'agentic-search-v3'94const agenticProxyModel = modelName || SearchSubagentToolCallingLoop.DEFAULT_AGENTIC_PROXY_MODEL;95return this.instantiationService.createInstance(ProxyAgenticEndpoint, agenticProxyModel);96}9798if (modelName) {99try {100// Try to get the specified model101return await this.endpointProvider.getChatEndpoint(modelName);102} catch (error) {103// Model not available or doesn't support tool calls, fallback to main agent104this._logService.warn(`Failed to get model ${modelName}, falling back to main agent endpoint: ${error}`);105return await this.endpointProvider.getChatEndpoint(this.options.request);106}107} else {108// No model name specified, use main agent endpoint109return await this.endpointProvider.getChatEndpoint(this.options.request);110}111}112113protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {114const endpoint = await this.getEndpoint();115// Use the effective tool call limit from options (already adjusted for thoroughness in the tool)116const maxSearchTurns = this.options.toolCallLimit;117const renderer = PromptRenderer.create(118this.instantiationService,119endpoint,120SearchSubagentPrompt,121{122promptContext: buildPromptContext,123maxSearchTurns,124thoroughness: this.options.thoroughness,125}126);127return await renderer.render(progress, token);128}129130protected async getAvailableTools(): Promise<LanguageModelToolInformation[]> {131const endpoint = await this.getEndpoint();132const allTools = this.toolsService.getEnabledTools(this.options.request, endpoint);133134// Only include tools relevant for search operations.135// We include semantic_search (Codebase) and the basic search primitives.136// The Codebase tool checks for inSubAgent context to prevent nested tool calling loops.137const allowedSearchTools = new Set([138ToolName.Codebase, // Semantic search139ToolName.FindFiles,140ToolName.FindTextInFiles,141ToolName.ReadFile142]);143144return allTools.filter(tool => allowedSearchTools.has(tool.name as ToolName));145}146147protected async fetch({ messages, finishedCb, requestOptions, modelCapabilities }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise<ChatResponse> {148const endpoint = await this.getEndpoint();149return endpoint.makeChatRequest2({150debugName: SearchSubagentToolCallingLoop.ID,151messages,152finishedCb,153location: this.options.location,154modelCapabilities: { ...modelCapabilities, reasoningEffort: undefined },155requestOptions: {156...requestOptions,157temperature: 0158},159// This loop is inside a tool called from another request, so never user initiated160userInitiatedRequest: false,161telemetryProperties: {162requestId: this.options.subAgentInvocationId,163messageId: randomUUID(),164messageSource: 'chat.editAgent',165subType: 'subagent/search',166conversationId: this.options.conversation.sessionId,167parentToolCallId: this.options.parentToolCallId,168parentHeaderRequestId: this.options.parentHeaderRequestId,169},170requestKindOptions: { kind: 'subagent' }171}, token);172}173}174175176