Skip to content

Commit ba4e28c

Browse files
authored
mcp: rework how virtual tool embeddings get expanded (#1164)
Our virtual-tool-group-ranking approach to expansion wasn't working as well as we expected it to. Previously we auto-expanded groups containing relevant tools to reach the budget. In this PR we instead put the top 10 most relevant tools at the top level of the tools list and leave the groups unchanged. I also added code to recalculat the embeddings when summarization happens.
1 parent e9e774d commit ba4e28c

File tree

6 files changed

+375
-44
lines changed

6 files changed

+375
-44
lines changed

src/extension/tools/common/virtualTools/toolGrouping.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import type { LanguageModelToolInformation } from 'vscode';
77
import { ConfigKey, HARD_TOOL_LIMIT, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
88
import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
99
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
10-
import { equals as arraysEqual } from '../../../../util/vs/base/common/arrays';
10+
import { equals as arraysEqual, uniqueFilter } from '../../../../util/vs/base/common/arrays';
1111
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
1212
import { Iterable } from '../../../../util/vs/base/common/iterator';
1313
import { IObservable } from '../../../../util/vs/base/common/observableInternal';
1414
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
1515
import { LanguageModelTextPart, LanguageModelToolResult } from '../../../../vscodeTypes';
16-
import { VIRTUAL_TOOL_NAME_PREFIX, VirtualTool } from './virtualTool';
16+
import { EMBEDDINGS_GROUP_NAME, VIRTUAL_TOOL_NAME_PREFIX, VirtualTool } from './virtualTool';
1717
import { VirtualToolGrouper } from './virtualToolGrouper';
1818
import * as Constant from './virtualToolsConstants';
1919
import { IToolCategorization, IToolGrouping } from './virtualToolTypes';
@@ -27,7 +27,7 @@ export function computeToolGroupingMinThreshold(experimentationService: IExperim
2727

2828
export class ToolGrouping implements IToolGrouping {
2929

30-
private readonly _root = new VirtualTool(VIRTUAL_TOOL_NAME_PREFIX, '', Infinity, { groups: [], toolsetKey: '', preExpanded: true });
30+
private readonly _root = new VirtualTool(VIRTUAL_TOOL_NAME_PREFIX, '', Infinity, { groups: [], toolsetKey: '', wasExpandedByDefault: true });
3131
protected _grouper: IToolCategorization = this._instantiationService.createInstance(VirtualToolGrouper);
3232
private _didToolsChange = true;
3333
private _turnNo = 0;
@@ -80,7 +80,9 @@ export class ToolGrouping implements IToolGrouping {
8080
"isVirtual": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether this called a virtual tool", "isMeasurement": true },
8181
"turnNo": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of turns into the loop when this expansion was made", "isMeasurement": true },
8282
"depth": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Nesting depth of the tool", "isMeasurement": true },
83-
"preExpanded": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the tool was pre-expanded or expanded on demand", "isMeasurement": true }
83+
"preExpanded": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the tool was pre-expanded or expanded on demand", "isMeasurement": true },
84+
"wasEmbedding": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the tool was pre-expanded due to an embedding", "isMeasurement": true },
85+
"totalTools": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Total number of tools available when this tool was called", "isMeasurement": true }
8486
}
8587
*/
8688
this._telemetryService.sendMSFTTelemetryEvent('virtualTools.called', {
@@ -89,7 +91,9 @@ export class ToolGrouping implements IToolGrouping {
8991
turnNo: localTurnNumber,
9092
isVirtual: tool instanceof VirtualTool ? 1 : 0,
9193
depth: path.length - 1,
92-
preExpanded: path.every(p => p.metadata.preExpanded) ? 1 : 0,
94+
preExpanded: path.every(p => p.metadata.wasExpandedByDefault) ? 1 : 0,
95+
wasEmbedding: path.some(p => p.name === EMBEDDINGS_GROUP_NAME) ? 1 : 0,
96+
totalTools: this._tools.length,
9397
});
9498
}
9599

@@ -124,7 +128,7 @@ export class ToolGrouping implements IToolGrouping {
124128

125129
async compute(query: string, token: CancellationToken): Promise<LanguageModelToolInformation[]> {
126130
await this._doCompute(query, token);
127-
return [...this._root.tools()];
131+
return [...this._root.tools()].filter(uniqueFilter(t => t.name));
128132
}
129133

130134
async computeAll(query: string, token: CancellationToken): Promise<(LanguageModelToolInformation | VirtualTool)[]> {
@@ -151,6 +155,7 @@ export class ToolGrouping implements IToolGrouping {
151155
let trimDownTo = HARD_TOOL_LIMIT;
152156

153157
if (this._trimOnNextCompute) {
158+
await this._grouper.recomputeEmbeddingRankings(query, this._root, token);
154159
trimDownTo = Constant.TRIM_THRESHOLD;
155160
this._trimOnNextCompute = false;
156161
}
@@ -159,12 +164,16 @@ export class ToolGrouping implements IToolGrouping {
159164

160165
while (Iterable.length(this._root.tools()) > trimDownTo) {
161166
const lowest = this._root.getLowestExpandedTool();
162-
if (!lowest || lowest === this._root) {
167+
if (!lowest || !isFinite(lowest.lastUsedOnTurn)) {
163168
break; // No more tools to trim.
164169
}
170+
if (lowest.metadata.canBeCollapsed === false) {
171+
lowest.lastUsedOnTurn = Infinity;
172+
continue;
173+
}
165174

166175
lowest.isExpanded = false;
167-
lowest.metadata.preExpanded = false;
176+
lowest.metadata.wasExpandedByDefault = false;
168177
}
169178
this._trimOnNextCompute = false;
170179
}

src/extension/tools/common/virtualTools/virtualTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import type { LanguageModelToolInformation } from 'vscode';
77
import { ISummarizedToolCategory } from './virtualToolTypes';
88

99
export const VIRTUAL_TOOL_NAME_PREFIX = 'activate_';
10+
export const EMBEDDINGS_GROUP_NAME = VIRTUAL_TOOL_NAME_PREFIX + 'embeddings';
1011

1112
export interface IVirtualToolMetadata {
1213
toolsetKey: string;
1314
possiblePrefix?: string;
1415
groups: ISummarizedToolCategory[];
15-
preExpanded?: boolean;
16+
wasExpandedByDefault?: boolean;
17+
canBeCollapsed?: boolean;
1618
}
1719

1820
export class VirtualTool {

src/extension/tools/common/virtualTools/virtualToolGrouper.ts

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { StopWatch } from '../../../../util/vs/base/common/stopwatch';
1818
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
1919
import { LanguageModelToolExtensionSource, LanguageModelToolMCPSource } from '../../../../vscodeTypes';
2020
import { EMBEDDING_TYPE_FOR_TOOL_GROUPING, ToolEmbeddingsComputer } from './toolEmbeddingsCache';
21-
import { VIRTUAL_TOOL_NAME_PREFIX, VirtualTool } from './virtualTool';
21+
import { EMBEDDINGS_GROUP_NAME, VIRTUAL_TOOL_NAME_PREFIX, VirtualTool } from './virtualTool';
2222
import { divideToolsIntoExistingGroups, divideToolsIntoGroups, summarizeToolGroup } from './virtualToolSummarizer';
2323
import { ISummarizedToolCategory, IToolCategorization, IToolGroupingCache } from './virtualToolTypes';
2424
import * as Constant from './virtualToolsConstants';
@@ -44,6 +44,10 @@ export class VirtualToolGrouper implements IToolCategorization {
4444
this.toolEmbeddingsComputer = _instantiationService.createInstance(ToolEmbeddingsComputer);
4545
}
4646

47+
private get virtualToolEmbeddingRankingEnabled() {
48+
return this._configurationService.getExperimentBasedConfig(ConfigKey.Internal.VirtualToolEmbeddingRanking, this._expService);
49+
}
50+
4751
async addGroups(query: string, root: VirtualTool, tools: LanguageModelToolInformation[], token: CancellationToken): Promise<void> {
4852
// If there's no need to group tools, just add them all directly;
4953
if (tools.length < Constant.START_GROUPING_AFTER_TOOL_COUNT) {
@@ -72,9 +76,8 @@ export class VirtualToolGrouper implements IToolCategorization {
7276
}
7377
}
7478

75-
const virtualToolEmbeddingRankingEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.Internal.VirtualToolEmbeddingRanking, this._expService);
7679
const predictedToolsSw = new StopWatch();
77-
const predictedToolsPromise = virtualToolEmbeddingRankingEnabled && this._getPredictedTools(query, tools, token).then(tools => ({ tools, durationMs: predictedToolsSw.elapsed() }));
80+
const predictedToolsPromise = this.virtualToolEmbeddingRankingEnabled && this._getPredictedTools(query, tools, token).then(tools => ({ tools, durationMs: predictedToolsSw.elapsed() }));
7881

7982
const grouped = await Promise.all(Object.entries(byToolset).map(([key, tools]) => {
8083
if (key === BUILT_IN_GROUP) {
@@ -92,7 +95,7 @@ export class VirtualToolGrouper implements IToolCategorization {
9295
const prev = previousGroups.get(tool.name);
9396
if (prev) {
9497
tool.isExpanded = prev.isExpanded;
95-
tool.metadata.preExpanded = prev.metadata.preExpanded;
98+
tool.metadata.wasExpandedByDefault = prev.metadata.wasExpandedByDefault;
9699
tool.lastUsedOnTurn = prev.lastUsedOnTurn;
97100
}
98101
}
@@ -101,6 +104,40 @@ export class VirtualToolGrouper implements IToolCategorization {
101104
await this._reExpandTools(root, predictedToolsPromise);
102105
}
103106

107+
async recomputeEmbeddingRankings(query: string, root: VirtualTool, token: CancellationToken): Promise<void> {
108+
if (!this.virtualToolEmbeddingRankingEnabled) {
109+
return;
110+
}
111+
112+
const predictedToolsSw = new StopWatch();
113+
114+
await this._reExpandTools(root, this._getPredictedTools(query, [...root.tools()], token).then(tools => ({
115+
tools,
116+
durationMs: predictedToolsSw.elapsed()
117+
})));
118+
}
119+
120+
private _addPredictedToolsGroup(root: VirtualTool, predictedTools: LanguageModelToolInformation[]): void {
121+
const newGroup = new VirtualTool(EMBEDDINGS_GROUP_NAME, 'Tools with high predicted relevancy for this query', Infinity, {
122+
toolsetKey: EMBEDDINGS_GROUP_NAME,
123+
wasExpandedByDefault: true,
124+
canBeCollapsed: false,
125+
groups: [],
126+
});
127+
128+
newGroup.isExpanded = true;
129+
for (const tool of predictedTools) {
130+
newGroup.contents.push(tool);
131+
}
132+
133+
const idx = root.contents.findIndex(t => t.name === EMBEDDINGS_GROUP_NAME);
134+
if (idx >= 0) {
135+
root.contents[idx] = newGroup;
136+
} else {
137+
root.contents.unshift(newGroup);
138+
}
139+
}
140+
104141
private async _reExpandTools(root: VirtualTool, predictedToolsPromise: Promise<{ tools: LanguageModelToolInformation[]; durationMs: number }> | false): Promise<void> {
105142
if (predictedToolsPromise) {
106143
// Aggressively expand groups with predicted tools up to hard limit
@@ -110,8 +147,7 @@ export class VirtualToolGrouper implements IToolCategorization {
110147
try {
111148
const { tools, durationMs } = await predictedToolsPromise;
112149
computeMs = durationMs;
113-
this._reExpandToolsToHitBudget(root, g => this._getGroupPredictedRelevancy(g, tools), HARD_TOOL_LIMIT);
114-
return;
150+
this._addPredictedToolsGroup(root, tools);
115151
} catch (e) {
116152
error = e;
117153
} finally {
@@ -161,30 +197,6 @@ export class VirtualToolGrouper implements IToolCategorization {
161197
return [...seen.values()];
162198
}
163199

164-
/**
165-
* Gets the predicted relevancy score for a group based on the highest priority predicted tool it contains.
166-
* Lower scores indicate higher relevancy (earlier in the predictedTools array).
167-
*/
168-
private _getGroupPredictedRelevancy(group: VirtualTool, predictedTools: LanguageModelToolInformation[]): number {
169-
// Create a set of predicted tool names for fast lookup
170-
const predictedToolNames = new Set(predictedTools.map(tool => tool.name));
171-
172-
// Create a map of tool name to its priority (index in predictedTools array)
173-
const toolPriority = new Map<string, number>();
174-
predictedTools.forEach((tool, index) => {
175-
toolPriority.set(tool.name, index);
176-
});
177-
178-
// Find the highest priority (lowest index) predicted tool in this group
179-
const priorities = group.contents
180-
.filter(tool => 'name' in tool && predictedToolNames.has(tool.name))
181-
.map(tool => toolPriority.get(tool.name) ?? Infinity)
182-
.filter(index => index !== Infinity);
183-
184-
// Return the highest priority (lowest index), or Infinity if no predicted tools
185-
return priorities.length > 0 ? Math.min(...priorities) : Infinity;
186-
}
187-
188200
/**
189201
* Eagerly expand groups when possible just to reduce the number of indirections.
190202
* Uses the provided ranker function to determine expansion priority.
@@ -215,7 +227,7 @@ export class VirtualToolGrouper implements IToolCategorization {
215227
}
216228

217229
vtool.isExpanded = true;
218-
vtool.metadata.preExpanded = true;
230+
vtool.metadata.wasExpandedByDefault = true;
219231
toolCount = nextCount;
220232

221233
if (toolCount > targetLimit) {

src/extension/tools/common/virtualTools/virtualToolTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ export interface IToolCategorization {
104104
* the appropriate virtual tool or top-level tool in the `root`.
105105
*/
106106
addGroups(query: string, root: VirtualTool, tools: LanguageModelToolInformation[], token: CancellationToken): Promise<void>;
107+
108+
/**
109+
* Recalculates the "embeddings" group, when enabled, so relevant tools
110+
* for the query are shown at the top level.
111+
*/
112+
recomputeEmbeddingRankings(query: string, root: VirtualTool, token: CancellationToken): Promise<void>;
107113
}
108114

109115
export interface ISummarizedToolCategory {

0 commit comments

Comments
 (0)