diff --git a/src/extension/intents/node/toolCallingLoop.ts b/src/extension/intents/node/toolCallingLoop.ts index edd6039d3b..841e4468a2 100644 --- a/src/extension/intents/node/toolCallingLoop.ts +++ b/src/extension/intents/node/toolCallingLoop.ts @@ -19,7 +19,7 @@ import { IFileSystemService } from '../../../platform/filesystem/common/fileSyst import { IGitService } from '../../../platform/git/common/gitService'; import { ILogService } from '../../../platform/log/common/logService'; import { isOpenAIContextManagementResponse, OpenAiFunctionDef } from '../../../platform/networking/common/fetch'; -import { IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; +import { IChatEndpoint, IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; import { OpenAIContextManagementResponse } from '../../../platform/networking/common/openai'; import { CopilotChatAttr, emitAgentTurnEvent, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, resolveWorkspaceOTelMetadata, StdAttr, truncateForOTel, workspaceMetadataToOTelAttributes } from '../../../platform/otel/common/index'; import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; @@ -213,11 +213,16 @@ export abstract class ToolCallingLoop { + return this._endpointProvider.getChatEndpoint(this.options.request); + } + /** Builds a prompt with the context. */ - protected abstract buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise; + protected abstract buildPrompt(endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise; /** Gets the tools that should be callable by the model. */ - protected abstract getAvailableTools(outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise; + protected abstract getAvailableTools(endpoint: IChatEndpoint, outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise; /** Creates the prompt context for the request. */ protected createPromptContext(availableTools: LanguageModelToolInformation[], outputStream: ChatResponseStream | undefined): Mutable { @@ -266,6 +271,7 @@ export abstract class ToolCallingLoop; @@ -1254,7 +1260,8 @@ export abstract class ToolCallingLoop { - let availableTools = await this.getAvailableTools(outputStream, token); + const endpoint = await this.resolveEndpoint(); + let availableTools = await this.getAvailableTools(endpoint, outputStream, token); // Emit tools_available on the agent span once, before the first CHAT span // starts in fetch(). This lets the debug logger write tools_*.json early. @@ -1271,14 +1278,14 @@ export abstract class ToolCallingLoop 0 ? await tokenizer.countToolTokens(availableTools) : 0; @@ -1403,7 +1409,7 @@ export abstract class ToolCallingLoop { @@ -1791,14 +1797,14 @@ export abstract class ToolCallingLoop { + private async buildPrompt2(endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, stream: ChatResponseStream | undefined, token: CancellationToken): Promise { const progress: Progress = { report(obj) { stream?.push(obj); } }; - const buildPromptResult = await this.buildPrompt(buildPromptContext, progress, token); + const buildPromptResult = await this.buildPrompt(endpoint, buildPromptContext, progress, token); for (const metadata of buildPromptResult.metadata.getAll(ToolResultMetadata)) { this.logToolResult(buildPromptContext, metadata); this.toolCallResults[metadata.toolCallId] = metadata.result; diff --git a/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts b/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts index 24d5a04821..57f918bd51 100644 --- a/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts +++ b/src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ChatRequest, LanguageModelToolInformation } from 'vscode'; import { IChatHookService } from '../../../../platform/chat/common/chatHookService'; import { ChatFetchResponseType, ChatResponse } from '../../../../platform/chat/common/commonTypes'; +import { IChatEndpoint } from '../../../../platform/networking/common/networking'; import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../../../util/vs/base/common/uuid'; @@ -24,15 +25,15 @@ import { MockChatHookService } from './toolCallingLoopHooks.spec'; * Concrete test implementation that exposes autopilot-related protected methods. */ class AutopilotTestToolCallingLoop extends ToolCallingLoop { - protected override async buildPrompt(_buildPromptContext: IBuildPromptContext): Promise { + protected override async buildPrompt(_endpoint: IChatEndpoint, _buildPromptContext: IBuildPromptContext): Promise { return nullRenderPromptResult(); } - protected override async getAvailableTools(): Promise { + protected override async getAvailableTools(_endpoint: IChatEndpoint): Promise { return []; } - protected override async fetch(): Promise { + protected override async fetch(_endpoint: IChatEndpoint): Promise { throw new Error('fetch should not be called in these tests'); } diff --git a/src/extension/intents/test/node/toolCallingLoopHooks.spec.ts b/src/extension/intents/test/node/toolCallingLoopHooks.spec.ts index e6e2e61380..dac5914037 100644 --- a/src/extension/intents/test/node/toolCallingLoopHooks.spec.ts +++ b/src/extension/intents/test/node/toolCallingLoopHooks.spec.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { CancellationToken, ChatHookResult, ChatHookType, ChatRequest, LanguageModelToolInformation } from 'vscode'; import { IChatHookService, SessionStartHookInput, StopHookInput, SubagentStartHookInput, SubagentStopHookInput } from '../../../../platform/chat/common/chatHookService'; +import { IChatEndpoint } from '../../../../platform/networking/common/networking'; import { NoopOTelService } from '../../../../platform/otel/common/noopOtelService'; import { resolveOTelConfig } from '../../../../platform/otel/common/otelConfig'; import { IOTelService } from '../../../../platform/otel/common/otelService'; @@ -101,16 +102,16 @@ class TestToolCallingLoop extends ToolCallingLoop { public lastBuildPromptContext: IBuildPromptContext | undefined; public additionalContextValue: string | undefined; - protected override async buildPrompt(buildPromptContext: IBuildPromptContext): Promise { + protected override async buildPrompt(_endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext): Promise { this.lastBuildPromptContext = buildPromptContext; return nullRenderPromptResult(); } - protected override async getAvailableTools(): Promise { + protected override async getAvailableTools(_endpoint: IChatEndpoint): Promise { return []; } - protected override async fetch(): Promise { + protected override async fetch(_endpoint: IChatEndpoint): Promise { throw new Error('fetch should not be called in these tests'); } diff --git a/src/extension/intents/test/node/toolCallingLoopUsage.spec.ts b/src/extension/intents/test/node/toolCallingLoopUsage.spec.ts index a7428f6e83..84ced7411d 100644 --- a/src/extension/intents/test/node/toolCallingLoopUsage.spec.ts +++ b/src/extension/intents/test/node/toolCallingLoopUsage.spec.ts @@ -7,6 +7,7 @@ import { Raw } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ChatRequest, LanguageModelChat, LanguageModelToolInformation } from 'vscode'; import { ChatFetchResponseType, ChatResponse } from '../../../../platform/chat/common/commonTypes'; +import { IChatEndpoint } from '../../../../platform/networking/common/networking'; import { toTextPart } from '../../../../platform/chat/common/globalStringUtils'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { ChatResponseStreamImpl } from '../../../../util/common/chatResponseStreamImpl'; @@ -44,18 +45,18 @@ class UsageCapturingStream extends ChatResponseStreamImpl { } class UsageTestToolCallingLoop extends ToolCallingLoop { - protected override async buildPrompt(_buildPromptContext: IBuildPromptContext): Promise { + protected override async buildPrompt(_endpoint: IChatEndpoint, _buildPromptContext: IBuildPromptContext): Promise { return { ...nullRenderPromptResult(), messages: [{ role: Raw.ChatRole.User, content: [toTextPart('hello world')] }], }; } - protected override async getAvailableTools(): Promise { + protected override async getAvailableTools(_endpoint: IChatEndpoint): Promise { return []; } - protected override async fetch(): Promise { + protected override async fetch(_endpoint: IChatEndpoint): Promise { return { type: ChatFetchResponseType.Success, value: 'test-response', diff --git a/src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx b/src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx index 63c57a97b2..4d04621715 100644 --- a/src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx +++ b/src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx @@ -17,6 +17,7 @@ import { IOTelService } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; +import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes'; import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop'; @@ -52,12 +53,11 @@ export class McpToolCallingLoop extends ToolCallingLoop { return await this.endpointProvider.getChatEndpoint('copilot-fast'); } - protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(); + protected async buildPrompt(endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { const renderer = PromptRenderer.create( this.instantiationService, endpoint, @@ -70,7 +70,7 @@ export class McpToolCallingLoop extends ToolCallingLoop { + protected async getAvailableTools(_endpoint: IChatEndpoint): Promise { if (this.options.conversation.turns.length > 5) { return []; // force a response } @@ -90,8 +90,7 @@ export class McpToolCallingLoop extends ToolCallingLoop { - const endpoint = await this.getEndpoint(); + protected async fetch(endpoint: IChatEndpoint, opts: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { return endpoint.makeChatRequest2({ ...opts, debugName: McpToolCallingLoop.ID, diff --git a/src/extension/prompt/node/codebaseToolCalling.ts b/src/extension/prompt/node/codebaseToolCalling.ts index 27135f3685..e3c02a0aa6 100644 --- a/src/extension/prompt/node/codebaseToolCalling.ts +++ b/src/extension/prompt/node/codebaseToolCalling.ts @@ -18,6 +18,7 @@ import { IOTelService } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; +import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes'; import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop'; @@ -56,7 +57,7 @@ export class CodebaseToolCallingLoop extends ToolCallingLoop { let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request); if (!endpoint.supportsToolCalls) { endpoint = await this.endpointProvider.getChatEndpoint('copilot-base'); @@ -64,8 +65,7 @@ export class CodebaseToolCallingLoop extends ToolCallingLoop, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(this.options.request); + protected async buildPrompt(endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { const renderer = PromptRenderer.create( this.instantiationService, endpoint, @@ -77,13 +77,11 @@ export class CodebaseToolCallingLoop extends ToolCallingLoop { - const endpoint = await this.getEndpoint(this.options.request); + protected async getAvailableTools(endpoint: IChatEndpoint): Promise { return this.toolsService.getEnabledTools(this.options.request, endpoint, tool => tool.tags.includes('vscode_codesearch')); } - protected async fetch({ messages, finishedCb, requestOptions }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(this.options.request); + protected async fetch(endpoint: IChatEndpoint, { messages, finishedCb, requestOptions }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { return endpoint.makeChatRequest( CodebaseToolCallingLoop.ID, messages, diff --git a/src/extension/prompt/node/defaultIntentRequestHandler.ts b/src/extension/prompt/node/defaultIntentRequestHandler.ts index bd63a1782b..0d1df7fb10 100644 --- a/src/extension/prompt/node/defaultIntentRequestHandler.ts +++ b/src/extension/prompt/node/defaultIntentRequestHandler.ts @@ -22,6 +22,7 @@ import { IOctoKitService } from '../../../platform/github/common/githubService'; import { HAS_IGNORED_FILES_MESSAGE } from '../../../platform/ignore/common/ignoreService'; import { ILogService } from '../../../platform/log/common/logService'; import { isAnthropicToolSearchEnabled } from '../../../platform/networking/common/anthropic'; +import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { FilterReason } from '../../../platform/networking/common/openai'; import { IOTelService } from '../../../platform/otel/common/otelService'; import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken'; @@ -679,13 +680,17 @@ class DefaultToolCallingLoop extends ToolCallingLoop { } } - protected override async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { + protected override async resolveEndpoint(): Promise { + return this.options.invocation.endpoint; + } + + protected override async buildPrompt(_endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { const buildPromptResult = await this.options.invocation.buildPrompt(buildPromptContext, progress, token); this.fixMessageNames(buildPromptResult.messages); return buildPromptResult; } - protected override async fetch(opts: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { + protected override async fetch(_endpoint: IChatEndpoint, opts: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { const messageSourcePrefix = this.options.location === ChatLocation.Editor ? 'inline' : 'chat'; const baseDebugName = this.options.request.subAgentInvocationId ? `tool/runSubagent${this.options.request.subAgentName ? `-${this.options.request.subAgentName}` : ''}` : @@ -729,7 +734,7 @@ class DefaultToolCallingLoop extends ToolCallingLoop { }, token); } - protected override async getAvailableTools(outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise { + protected override async getAvailableTools(_endpoint: IChatEndpoint, outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise { const tools = await this.options.invocation.getAvailableTools?.() ?? []; // Skip tool grouping when Anthropic tool search is enabled diff --git a/src/extension/prompt/node/executionSubagentToolCallingLoop.ts b/src/extension/prompt/node/executionSubagentToolCallingLoop.ts index 313b9359e7..8b992d91c3 100644 --- a/src/extension/prompt/node/executionSubagentToolCallingLoop.ts +++ b/src/extension/prompt/node/executionSubagentToolCallingLoop.ts @@ -18,6 +18,7 @@ import { IOTelService } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; +import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes'; import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop'; @@ -77,7 +78,7 @@ export class ExecutionSubagentToolCallingLoop extends ToolCallingLoop { const modelName = this._configurationService.getConfig(ConfigKey.Advanced.ExecutionSubagentModel) as ChatEndpointFamily; if (modelName) { try { @@ -97,8 +98,7 @@ export class ExecutionSubagentToolCallingLoop extends ToolCallingLoop, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(); + protected async buildPrompt(endpoint: IChatEndpoint, buildpromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { const maxExecutionTurns = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.ExecutionSubagentToolCallLimit, this._experimentationService); const renderer = PromptRenderer.create( this.instantiationService, @@ -112,8 +112,7 @@ export class ExecutionSubagentToolCallingLoop extends ToolCallingLoop { - const endpoint = await this.getEndpoint(); + protected async getAvailableTools(endpoint: IChatEndpoint): Promise { const allTools = this.toolsService.getEnabledTools(this.options.request, endpoint); const allowedExecutionTools = new Set([ @@ -123,8 +122,7 @@ export class ExecutionSubagentToolCallingLoop extends ToolCallingLoop allowedExecutionTools.has(tool.name as ToolName)); } - protected async fetch({ messages, finishedCb, requestOptions, enableThinking, reasoningEffort }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(); + protected async fetch(endpoint: IChatEndpoint, { messages, finishedCb, requestOptions, enableThinking, reasoningEffort }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { return endpoint.makeChatRequest2({ debugName: ExecutionSubagentToolCallingLoop.ID, messages, diff --git a/src/extension/prompt/node/searchSubagentToolCallingLoop.ts b/src/extension/prompt/node/searchSubagentToolCallingLoop.ts index 2549ef2e68..39bc6a9b6e 100644 --- a/src/extension/prompt/node/searchSubagentToolCallingLoop.ts +++ b/src/extension/prompt/node/searchSubagentToolCallingLoop.ts @@ -19,6 +19,7 @@ import { IOTelService } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; +import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes'; import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop'; @@ -80,33 +81,28 @@ export class SearchSubagentToolCallingLoop extends ToolCallingLoop { const modelName = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentModel, this._experimentationService) as ChatEndpointFamily | undefined; const useAgenticProxy = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentUseAgenticProxy, this._experimentationService); if (useAgenticProxy) { - // Use agentic proxy with SearchSubagentModel or default to 'agentic-search-v3' const agenticProxyModel = modelName || SearchSubagentToolCallingLoop.DEFAULT_AGENTIC_PROXY_MODEL; return this.instantiationService.createInstance(ProxyAgenticSearchEndpoint, agenticProxyModel); } if (modelName) { try { - // Try to get the specified model return await this.endpointProvider.getChatEndpoint(modelName); } catch (error) { - // Model not available or doesn't support tool calls, fallback to main agent this._logService.warn(`Failed to get model ${modelName}, falling back to main agent endpoint: ${error}`); return await this.endpointProvider.getChatEndpoint(this.options.request); } } else { - // No model name specified, use main agent endpoint return await this.endpointProvider.getChatEndpoint(this.options.request); } } - protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(); + protected async buildPrompt(endpoint: IChatEndpoint, buildPromptContext: IBuildPromptContext, progress: Progress, token: CancellationToken): Promise { const maxSearchTurns = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SearchSubagentToolCallLimit, this._experimentationService); const renderer = PromptRenderer.create( this.instantiationService, @@ -120,15 +116,11 @@ export class SearchSubagentToolCallingLoop extends ToolCallingLoop { - const endpoint = await this.getEndpoint(); + protected async getAvailableTools(endpoint: IChatEndpoint): Promise { const allTools = this.toolsService.getEnabledTools(this.options.request, endpoint); - // Only include tools relevant for search operations. - // We include semantic_search (Codebase) and the basic search primitives. - // The Codebase tool checks for inSubAgent context to prevent nested tool calling loops. const allowedSearchTools = new Set([ - ToolName.Codebase, // Semantic search + ToolName.Codebase, ToolName.FindFiles, ToolName.FindTextInFiles, ToolName.ReadFile @@ -137,8 +129,7 @@ export class SearchSubagentToolCallingLoop extends ToolCallingLoop allowedSearchTools.has(tool.name as ToolName)); } - protected async fetch({ messages, finishedCb, requestOptions, enableThinking, reasoningEffort }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { - const endpoint = await this.getEndpoint(); + protected async fetch(endpoint: IChatEndpoint, { messages, finishedCb, requestOptions, enableThinking, reasoningEffort }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise { return endpoint.makeChatRequest2({ debugName: SearchSubagentToolCallingLoop.ID, messages,