Skip to content
Open
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 @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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,
};
}

Expand All @@ -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(
Expand All @@ -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');
Expand All @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ export interface IChatSubagentToolInvocationData {
agentName?: string;
prompt?: string;
result?: string;
modelName?: string;
}

export interface IChatTodoListContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -60,6 +78,9 @@ export class RunSubagentTool extends Disposable implements IToolImpl {

readonly onDidUpdateToolData: Event<IConfigurationChangeEvent>;

/** Hack to port data between prepare/invoke */
private readonly _resolvedModels = new Map<string, { modeModelId: string | undefined; resolvedModelName: string | undefined }>();

constructor(
@IChatAgentService private readonly chatAgentService: IChatAgentService,
@IChatService private readonly chatService: IChatService,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -288,6 +318,7 @@ export class RunSubagentTool extends Disposable implements IToolImpl {
subAgentInvocationId,
description: args.description,
agentName: agentRequest.subAgentName,
modelName: resolvedModelName,
}
};

Expand All @@ -305,18 +336,64 @@ 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<IPreparedToolInvocation | undefined> {
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: {
kind: 'subagent',
description: args.description,
agentName: subagent?.name,
prompt: args.prompt,
modelName: resolved.resolvedModelName,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading
Loading