From 7becd3f98a878248fa4b355283f3f89437c90424 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 6 Feb 2026 14:26:53 -0800 Subject: [PATCH] Limit the subagent model to a less expensive one than the main agent Fix microsoft/vscode-internalbacklog#6732 --- .../tools/languageModelToolsService.ts | 4 +- .../chatCollapsibleContentPart.ts | 2 +- .../chatSubagentContentPart.ts | 42 ++++++- .../chat/common/chatService/chatService.ts | 1 + .../tools/builtinTools/runSubagentTool.ts | 105 +++++++++++++++--- .../common/tools/languageModelToolsService.ts | 2 + .../chatSubagentContentPart.test.ts | 92 +++++++++++++++ .../builtinTools/runSubagentTool.test.ts | 56 +++++++++- .../tools/builtinTools/fetchPageTool.test.ts | 6 +- 9 files changed, 284 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 74661699be856..5c12ae5446bfa 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -581,10 +581,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (tool.impl!.prepareToolInvocation) { const preparePromise = tool.impl!.prepareToolInvocation({ parameters: dto.parameters, + toolCallId: dto.callId, chatRequestId: dto.chatRequestId, chatSessionId: dto.context?.sessionId, chatSessionResource: dto.context?.sessionResource, - chatInteractionId: dto.chatInteractionId + chatInteractionId: dto.chatInteractionId, + modelId: dto.modelId, }, token); const raceResult = await Promise.race([ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts index ff0da48563edc..01559966fdfd6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts @@ -49,7 +49,7 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I private title: IMarkdownString | string, context: IChatContentPartRenderContext, private readonly hoverMessage: IMarkdownString | undefined, - @IHoverService private readonly hoverService: IHoverService, + @IHoverService protected readonly hoverService: IHoverService, ) { super(); this.element = context.element; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index 25b4bcac405db..429a09fdd41ec 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -6,7 +6,7 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { rcut } from '../../../../../../base/common/strings.js'; import { localize } from '../../../../../../nls.js'; @@ -79,6 +79,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Current tool message for collapsed title (persists even after tool completes) private currentRunningToolMessage: string | undefined; + // Model name used by this subagent for hover tooltip + private modelName: string | undefined; + private readonly _hoverDisposable = this._register(new MutableDisposable()); + // Confirmation auto-expand tracking private toolsWaitingForConfirmation: number = 0; private userManuallyExpanded: boolean = false; @@ -87,11 +91,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen /** * Extracts subagent info (description, agentName, prompt) from a tool invocation. */ - private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined } { + private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } { const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent...'); if (toolInvocation.toolId !== RunSubagentTool.Id) { - return { description: defaultDescription, agentName: undefined, prompt: undefined }; + return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined }; } // Check toolSpecificData first (works for both live and serialized) @@ -100,6 +104,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen description: toolInvocation.toolSpecificData.description ?? defaultDescription, agentName: toolInvocation.toolSpecificData.agentName, prompt: toolInvocation.toolSpecificData.prompt, + modelName: toolInvocation.toolSpecificData.modelName, }; } @@ -113,10 +118,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen description: params?.description ?? defaultDescription, agentName: params?.agentName, prompt: params?.prompt, + modelName: undefined, }; } - return { description: defaultDescription, agentName: undefined, prompt: undefined }; + return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined }; } constructor( @@ -134,7 +140,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen @IHoverService hoverService: IHoverService, ) { // Extract description, agentName, and prompt from toolInvocation - const { description, agentName, prompt } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); + const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); // Build title: "AgentName: description" or "Subagent: description" const prefix = agentName || localize('chat.subagent.prefix', 'Subagent'); @@ -144,6 +150,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.description = description; this.agentName = agentName; this.prompt = prompt; + this.modelName = modelName; this.isInitiallyComplete = this.element.isComplete; const node = this.domNode; @@ -201,6 +208,9 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Scheduler for coalescing layout operations this.layoutScheduler = this._register(new AnimationFrameScheduler(this.domNode, () => this.performLayout())); + // Set up hover tooltip with model name if available + this.updateHover(); + // Render the prompt section at the start if available (must be after wrapper is initialized) this.renderPromptSection(); @@ -327,6 +337,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.setTitleWithWidgets(new MarkdownString(finalLabel), this.instantiationService, this.chatMarkdownAnchorService, this.chatContentMarkdownRenderer); } + private updateHover(): void { + if (!this.modelName || !this._collapseButton) { + return; + } + + this._hoverDisposable.value = this.hoverService.setupDelayedHover(this._collapseButton.element, { + content: localize('chat.subagent.modelTooltip', 'Model: {0}', this.modelName), + }); + } + /** * Tracks a tool invocation's state for: * 1. Updating the title with the current tool message (persists even after completion) @@ -400,15 +420,25 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen this.renderResultText(textParts.join('\n')); } + // Update model name from toolSpecificData (set during invoke()) + if (toolInvocation.toolSpecificData?.kind === 'subagent' && toolInvocation.toolSpecificData.modelName) { + this.modelName = toolInvocation.toolSpecificData.modelName; + this.updateHover(); + } + // Mark as inactive when the tool completes this.markAsInactive(); } else if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { wasStreaming = false; // Update things that change when tool is done streaming - const { description, agentName, prompt } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); + const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation); this.description = description; this.agentName = agentName; this.prompt = prompt; + if (modelName) { + this.modelName = modelName; + this.updateHover(); + } this.renderPromptSection(); this.updateTitle(); } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 75b4f81832af4..2e948c69d00a4 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -816,6 +816,7 @@ export interface IChatSubagentToolInvocationData { agentName?: string; prompt?: string; result?: string; + modelName?: string; } export interface IChatTodoListContent { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index 874c233010e40..ce0335e5a3700 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -40,6 +40,24 @@ import { ManageTodoListToolToolId } from './manageTodoListTool.js'; import { createToolSimpleTextResult } from './toolHelpers.js'; import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.js'; +/** + * Parses a multiplier string like "2X" or "0.5X" to a number. + * Returns 1 if parsing fails. + */ +export function parseMultiplier(multiplier: string | undefined): number { + if (!multiplier) { + // No multiplier- byok? + return 1000; + } + const match = /^([\d.]+)\s*[xX]$/i.exec(multiplier.trim()); + if (!match) { + // Doesn't match 3x pattern, maybe auto mode + return 1; + } + const value = parseFloat(match[1]); + return isNaN(value) ? 1 : value; +} + const BaseModelDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. This tool is good at researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use this agent to perform the search for you. - Agents do not run async or in the background, you will wait for the agent\'s result. @@ -60,6 +78,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl { readonly onDidUpdateToolData: Event; + /** Hack to port data between prepare/invoke */ + private readonly _resolvedModels = new Map(); + constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatService private readonly chatService: IChatService, @@ -143,25 +164,23 @@ export class RunSubagentTool extends Disposable implements IToolImpl { let modeTools = invocation.userSelectedTools; let modeInstructions: IChatRequestModeInstructions | undefined; let subagent: ICustomAgent | undefined; + let resolvedModelName: string | undefined; const subAgentName = args.agentName; if (subAgentName) { subagent = await this.getSubAgentByName(subAgentName); if (subagent) { - // Use mode-specific model if available - const modeModelQualifiedNames = subagent.model; - if (modeModelQualifiedNames) { - // Find the actual model identifier from the qualified name(s) - outer: for (const qualifiedName of modeModelQualifiedNames) { - const lmByQualifiedName = this.languageModelsService.lookupLanguageModelByQualifiedName(qualifiedName); - for (const fullId of this.languageModelsService.getLanguageModelIds()) { - const lmById = this.languageModelsService.lookupLanguageModel(fullId); - if (lmById && lmById?.id === lmByQualifiedName?.id) { - modeModelId = fullId; - break outer; - } - } - } + // Check the pre-resolved model cache from prepareToolInvocation + const cached = this._resolvedModels.get(invocation.callId); + if (cached) { + this._resolvedModels.delete(invocation.callId); + modeModelId = cached.modeModelId; + resolvedModelName = cached.resolvedModelName; + } else { + // Fallback: resolve the model here if prepare didn't cache it + const resolved = this.resolveSubagentModel(subagent, invocation.modelId); + modeModelId = resolved.modeModelId; + resolvedModelName = resolved.resolvedModelName; } // Use mode-specific tools if available @@ -188,6 +207,16 @@ export class RunSubagentTool extends Disposable implements IToolImpl { } else { throw new Error(`Requested agent '${subAgentName}' not found. Try again with the correct agent name, or omit the agentName to use the current agent.`); } + } else { + // No subagent name - clean up any cached entry and resolve model name from main model + const cached = this._resolvedModels.get(invocation.callId); + if (cached) { + this._resolvedModels.delete(invocation.callId); + resolvedModelName = cached.resolvedModelName; + } else { + const resolvedModelMetadata = modeModelId ? this.languageModelsService.lookupLanguageModel(modeModelId) : undefined; + resolvedModelName = resolvedModelMetadata?.name; + } } // Track whether we should collect markdown (after the last tool invocation) @@ -276,6 +305,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { // Store result in toolSpecificData for serialization if (invocation.toolSpecificData?.kind === 'subagent') { invocation.toolSpecificData.result = resultText; + invocation.toolSpecificData.modelName = resolvedModelName; } // Return result with toolMetadata containing subAgentInvocationId for trajectory tracking @@ -288,6 +318,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { subAgentInvocationId, description: args.description, agentName: agentRequest.subAgentName, + modelName: resolvedModelName, } }; @@ -305,11 +336,56 @@ export class RunSubagentTool extends Disposable implements IToolImpl { return agents.find(agent => agent.name === name); } + /** + * Resolves the model to be used by a subagent, applying multiplier-based + * fallback to avoid using a more expensive model than the main agent. + */ + private resolveSubagentModel(subagent: ICustomAgent | undefined, mainModelId: string | undefined): { modeModelId: string | undefined; resolvedModelName: string | undefined } { + let modeModelId = mainModelId; + + if (subagent) { + const modeModelQualifiedNames = subagent.model; + if (modeModelQualifiedNames) { + // Find the actual model identifier from the qualified name(s) + outer: for (const qualifiedName of modeModelQualifiedNames) { + const lmByQualifiedName = this.languageModelsService.lookupLanguageModelByQualifiedName(qualifiedName); + for (const fullId of this.languageModelsService.getLanguageModelIds()) { + const lmById = this.languageModelsService.lookupLanguageModel(fullId); + if (lmById && lmById?.id === lmByQualifiedName?.id) { + modeModelId = fullId; + break outer; + } + } + } + } + + // If the subagent's model has a larger multiplier than the main agent's model, + // fall back to the main agent's model to avoid using a more expensive model. + if (modeModelId && modeModelId !== mainModelId) { + const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined; + const subagentModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); + const mainMultiplier = parseMultiplier(mainModelMetadata?.multiplier); + const subagentMultiplier = parseMultiplier(subagentModelMetadata?.multiplier); + if (subagentMultiplier > mainMultiplier) { + this.logService.warn(`[RunSubagentTool] Subagent '${subagent.name}' requested model '${subagentModelMetadata?.name}' (multiplier: ${subagentModelMetadata?.multiplier ?? 'unknown'}) which has a larger multiplier than the main agent model '${mainModelMetadata?.name}' (multiplier: ${mainModelMetadata?.multiplier ?? 'unknown'}). Falling back to the main agent model.`); + modeModelId = mainModelId; + } + } + } + + const resolvedModelMetadata = modeModelId ? this.languageModelsService.lookupLanguageModel(modeModelId) : undefined; + return { modeModelId, resolvedModelName: resolvedModelMetadata?.name }; + } + async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const args = context.parameters as IRunSubagentToolInputParams; const subagent = args.agentName ? await this.getSubAgentByName(args.agentName) : undefined; + // Resolve the model early and cache it for invoke() + const resolved = this.resolveSubagentModel(subagent, context.modelId); + this._resolvedModels.set(context.toolCallId, resolved); + return { invocationMessage: args.description, toolSpecificData: { @@ -317,6 +393,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl { description: args.description, agentName: subagent?.name, prompt: args.prompt, + modelName: resolved.resolvedModelName, }, }; } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index c3eb419ca6813..642cca88efc03 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -198,11 +198,13 @@ export function isToolInvocationContext(obj: any): obj is IToolInvocationContext export interface IToolInvocationPreparationContext { // eslint-disable-next-line @typescript-eslint/no-explicit-any parameters: any; + toolCallId: string; chatRequestId?: string; /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; chatSessionResource: URI | undefined; chatInteractionId?: string; + modelId?: string; } export type ToolInputOutputBase = { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index 6844fd2cee02a..35b7bdf710680 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -1365,4 +1365,96 @@ suite('ChatSubagentContentPart', () => { 'Title should still include tool message after completion'); }); }); + + suite('Model name tooltip', () => { + test('should set up hover with model name from serialized toolSpecificData', () => { + const setupDelayedHoverCalls: { element: HTMLElement; content: string }[] = []; + mockHoverService.setupDelayedHover = (element: HTMLElement, options: { content: string }) => { + setupDelayedHoverCalls.push({ element, content: typeof options.content === 'string' ? options.content : '' }); + return { dispose: () => { } }; + }; + + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + modelName: 'GPT-4o' + } + }); + const context = createMockRenderContext(true); + + createPart(serializedInvocation, context); + + // Should have set up a hover with the model name + const modelHover = setupDelayedHoverCalls.find(c => c.content.includes('GPT-4o')); + assert.ok(modelHover, 'Should set up hover with model name'); + }); + + test('should not set up hover when no model name is available', () => { + const setupDelayedHoverCalls: { element: HTMLElement; content: string }[] = []; + mockHoverService.setupDelayedHover = (element: HTMLElement, options: { content: string }) => { + setupDelayedHoverCalls.push({ element, content: typeof options.content === 'string' ? options.content : '' }); + return { dispose: () => { } }; + }; + + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'TestAgent', + prompt: 'Do the thing', + result: 'Done', + // no modelName + } + }); + const context = createMockRenderContext(true); + + createPart(serializedInvocation, context); + + // Should not have set up any hover with model info + const modelHover = setupDelayedHoverCalls.find(c => c.content.includes('Model:')); + assert.strictEqual(modelHover, undefined, 'Should not set up model hover when no model name'); + }); + + test('should set up hover when tool completes and toolSpecificData has modelName', () => { + const setupDelayedHoverCalls: { element: HTMLElement; content: string }[] = []; + mockHoverService.setupDelayedHover = (element: HTMLElement, options: { content: string }) => { + setupDelayedHoverCalls.push({ element, content: typeof options.content === 'string' ? options.content : '' }); + return { dispose: () => { } }; + }; + + const toolSpecificData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent', + prompt: 'Do stuff', + }; + + const toolInvocation = createMockToolInvocation({ + toolSpecificData, + stateType: IChatToolInvocation.StateKind.Executing, + }); + const context = createMockRenderContext(false); + + createPart(toolInvocation, context); + + // No model hover initially (no modelName yet) + const initialHover = setupDelayedHoverCalls.find(c => c.content.includes('Model:')); + assert.strictEqual(initialHover, undefined, 'Should not have model hover initially'); + + // Simulate invoke() setting modelName on toolSpecificData + toolSpecificData.modelName = 'Claude Sonnet 4'; + + // Simulate tool completion + const state = toolInvocation.state as ReturnType>; + state.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + + // Should now have a hover with the model name + const modelHover = setupDelayedHoverCalls.find(c => c.content.includes('Claude Sonnet 4')); + assert.ok(modelHover, 'Should set up hover with model name after completion'); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts index 25d7088d46e17..0251260fd7cc9 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -9,7 +9,7 @@ import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../../../platform/log/common/log.js'; import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { RunSubagentTool } from '../../../../common/tools/builtinTools/runSubagentTool.js'; +import { RunSubagentTool, parseMultiplier } from '../../../../common/tools/builtinTools/runSubagentTool.js'; import { MockLanguageModelToolsService } from '../mockLanguageModelToolsService.js'; import { IChatAgentService } from '../../../../common/participants/chatAgents.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; @@ -21,6 +21,58 @@ import { MockPromptsService } from '../../promptSyntax/service/mockPromptsServic suite('RunSubagentTool', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + suite('parseMultiplier', () => { + test('parses standard multiplier strings', () => { + assert.deepStrictEqual( + [ + { input: '1X', expected: 1 }, + { input: '2X', expected: 2 }, + { input: '0.5X', expected: 0.5 }, + { input: '1.5x', expected: 1.5 }, + { input: '10X', expected: 10 }, + { input: '0.25x', expected: 0.25 }, + ].map(({ input, expected }) => ({ input, result: parseMultiplier(input), expected })), + [ + { input: '1X', result: 1, expected: 1 }, + { input: '2X', result: 2, expected: 2 }, + { input: '0.5X', result: 0.5, expected: 0.5 }, + { input: '1.5x', result: 1.5, expected: 1.5 }, + { input: '10X', result: 10, expected: 10 }, + { input: '0.25x', result: 0.25, expected: 0.25 }, + ] + ); + }); + + test('handles whitespace in multiplier strings', () => { + assert.strictEqual(parseMultiplier(' 2X '), 2); + assert.strictEqual(parseMultiplier('1.5 X'), 1.5); + }); + + test('returns 1000 for undefined and 1 for empty input', () => { + assert.strictEqual(parseMultiplier(undefined), 1000); + assert.strictEqual(parseMultiplier(''), 1000); + }); + + test('returns 1 for invalid formats', () => { + assert.deepStrictEqual( + [ + { input: 'abc', expected: 1 }, + { input: 'X', expected: 1 }, + { input: '2', expected: 1 }, + { input: 'fast', expected: 1 }, + { input: '2Y', expected: 1 }, + ].map(({ input, expected }) => ({ input, result: parseMultiplier(input), expected })), + [ + { input: 'abc', result: 1, expected: 1 }, + { input: 'X', result: 1, expected: 1 }, + { input: '2', result: 1, expected: 1 }, + { input: 'fast', result: 1, expected: 1 }, + { input: '2Y', result: 1, expected: 1 }, + ] + ); + }); + }); + suite('resultText trimming', () => { test('trims leading empty codeblocks (```\\n```) from result', () => { // This tests the regex: /^\n*```\n+```\n*/g @@ -77,6 +129,7 @@ suite('RunSubagentTool', () => { description: 'Test task', agentName: 'CustomAgent', }, + toolCallId: 'test-call-1', chatSessionResource: URI.parse('test://session'), }, CancellationToken.None @@ -89,6 +142,7 @@ suite('RunSubagentTool', () => { description: 'Test task', agentName: 'CustomAgent', prompt: 'Test prompt', + modelName: undefined, }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts index acee94cc06c5a..1281978984655 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts @@ -192,7 +192,7 @@ suite('FetchWebPageTool', () => { ); const preparation = await tool.prepareToolInvocation( - { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] }, chatSessionResource: undefined }, + { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] }, toolCallId: 'test-call-1', chatSessionResource: undefined }, CancellationToken.None ); @@ -230,7 +230,7 @@ suite('FetchWebPageTool', () => { ); const preparation1 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://example.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, + { parameters: { urls: ['https://example.com'] }, toolCallId: 'test-call-2', chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None ); @@ -238,7 +238,7 @@ suite('FetchWebPageTool', () => { assert.strictEqual(preparation1.confirmationMessages?.title, undefined); const preparation2 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://other.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, + { parameters: { urls: ['https://other.com'] }, toolCallId: 'test-call-3', chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None );