Skip to content

Commit 17ea4de

Browse files
PenguinDOOMCopilot
andcommitted
fix: align tool search with request snapshot
Co-authored-by: Copilot <copilot@github.com>
1 parent 8cfeeee commit 17ea4de

2 files changed

Lines changed: 76 additions & 32 deletions

File tree

extensions/copilot/src/extension/tools/node/test/toolSearchTool.spec.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ suite('ToolSearchTool', () => {
3535
return tools.map(candidate => candidate.name);
3636
},
3737
} as any,
38-
{
39-
tools: [
40-
makeToolInfo('read_file'),
41-
makeToolInfo('enabled_deferred_tool'),
42-
makeToolInfo('disabled_deferred_tool'),
43-
],
44-
} as any,
4538
{
4639
isNonDeferredTool: (name: string) => nonDeferred.has(name),
4740
} as any,
@@ -81,13 +74,6 @@ suite('ToolSearchTool', () => {
8174
return tools.map(candidate => candidate.name);
8275
},
8376
} as any,
84-
{
85-
tools: [
86-
makeToolInfo('read_file'),
87-
makeToolInfo('request_a_tool'),
88-
makeToolInfo('request_b_tool'),
89-
],
90-
} as any,
9177
{
9278
isNonDeferredTool: (name: string) => nonDeferred.has(name),
9379
} as any,
@@ -132,6 +118,45 @@ suite('ToolSearchTool', () => {
132118
expect(searchedToolNames).toEqual([['request_a_tool']]);
133119
});
134120

121+
test('retains request-scoped virtual activate groups during tool search candidate selection', async () => {
122+
const searchedToolNames: string[][] = [];
123+
const nonDeferred = new Set(['read_file']);
124+
const tool = new ToolSearchTool(
125+
{
126+
searchToolsByQuery: async (_query: string, tools: readonly vscode.LanguageModelToolInformation[]) => {
127+
searchedToolNames.push(tools.map(candidate => candidate.name));
128+
return tools.map(candidate => candidate.name);
129+
},
130+
} as any,
131+
{
132+
isNonDeferredTool: (name: string) => nonDeferred.has(name),
133+
} as any,
134+
{ trace() { } } as any,
135+
);
136+
137+
const resolvedInput = await (tool as any).resolveInput?.(
138+
{ query: 'vscode interaction tools' },
139+
{
140+
tools: {
141+
toolReferences: [],
142+
toolInvocationToken: undefined as never,
143+
availableTools: [
144+
makeToolInfo('read_file'),
145+
makeToolInfo('activate_vs_code_interaction'),
146+
],
147+
},
148+
},
149+
0,
150+
);
151+
152+
await tool.invoke(
153+
{ input: resolvedInput } as vscode.LanguageModelToolInvocationOptions<{ query: string }>,
154+
{ isCancellationRequested: false } as vscode.CancellationToken,
155+
);
156+
157+
expect(searchedToolNames).toEqual([['activate_vs_code_interaction']]);
158+
});
159+
135160
test('fails explicitly when invoke runs without request-scoped resolveInput context', async () => {
136161
const nonDeferred = new Set(['read_file']);
137162
const tool = new ToolSearchTool(
@@ -140,11 +165,6 @@ suite('ToolSearchTool', () => {
140165
throw new Error('search should not run without resolveInput context');
141166
},
142167
} as any,
143-
{
144-
tools: [
145-
makeToolInfo('enabled_deferred_tool'),
146-
],
147-
} as any,
148168
{
149169
isNonDeferredTool: (name: string) => nonDeferred.has(name),
150170
} as any,
@@ -156,6 +176,36 @@ suite('ToolSearchTool', () => {
156176
{ isCancellationRequested: false } as vscode.CancellationToken,
157177
)).rejects.toThrow('ToolSearchTool: request-scoped deferred tools are unavailable. Ensure resolveInput is called before invoke.');
158178
});
179+
180+
test('uses an empty deferred tool snapshot when promptContext.tools is missing', async () => {
181+
const searchedToolNames: string[][] = [];
182+
const tool = new ToolSearchTool(
183+
{
184+
searchToolsByQuery: async (_query: string, tools: readonly vscode.LanguageModelToolInformation[]) => {
185+
searchedToolNames.push(tools.map(candidate => candidate.name));
186+
return tools.map(candidate => candidate.name);
187+
},
188+
} as any,
189+
{
190+
isNonDeferredTool: () => false,
191+
} as any,
192+
{ trace() { } } as any,
193+
);
194+
195+
const resolvedInput = await tool.resolveInput(
196+
{ query: 'anything' },
197+
{} as any,
198+
0,
199+
);
200+
201+
const result = await tool.invoke(
202+
{ input: resolvedInput } as vscode.LanguageModelToolInvocationOptions<{ query: string }>,
203+
{ isCancellationRequested: false } as vscode.CancellationToken,
204+
);
205+
206+
expect(searchedToolNames).toEqual([[]]);
207+
expect(result.content).toEqual([expect.objectContaining({ value: '[]' })]);
208+
});
159209
});
160210

161211
suite('TestToolsService getEnabledTools', () => {

extensions/copilot/src/extension/tools/node/toolSearchTool.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeT
1212
import { IBuildPromptContext } from '../../prompt/common/intents';
1313
import { createRequestToolManifest } from '../common/requestToolManifest';
1414
import { CopilotToolMode, ICopilotModelSpecificTool, ToolRegistry } from '../common/toolsRegistry';
15-
import { IToolsService } from '../common/toolsService';
1615
import { IToolEmbeddingsComputer } from '../common/virtualTools/toolEmbeddingsComputer';
1716

1817
export interface IToolSearchParams {
@@ -21,16 +20,15 @@ export interface IToolSearchParams {
2120
}
2221

2322
const DEFAULT_SEARCH_LIMIT = 5;
24-
const requestScopedDeferredToolNamesKey = Symbol('requestScopedDeferredToolNames');
23+
const requestScopedDeferredToolsKey = Symbol('requestScopedDeferredTools');
2524

2625
type ResolvedToolSearchParams = IToolSearchParams & {
27-
[requestScopedDeferredToolNamesKey]?: readonly string[];
26+
[requestScopedDeferredToolsKey]?: readonly vscode.LanguageModelToolInformation[];
2827
};
2928

3029
export class ToolSearchTool implements ICopilotModelSpecificTool<IToolSearchParams> {
3130
constructor(
3231
@IToolEmbeddingsComputer private readonly _toolEmbeddingsComputer: IToolEmbeddingsComputer,
33-
@IToolsService private readonly _toolsService: IToolsService,
3432
@IToolDeferralService private readonly _toolDeferralService: IToolDeferralService,
3533
@ILogService private readonly _logService: ILogService,
3634
) { }
@@ -44,19 +42,15 @@ export class ToolSearchTool implements ICopilotModelSpecificTool<IToolSearchPara
4442
]);
4543
}
4644

47-
const requestScopedDeferredToolNames = (options.input as ResolvedToolSearchParams)[requestScopedDeferredToolNamesKey];
45+
const requestScopedDeferredTools = (options.input as ResolvedToolSearchParams)[requestScopedDeferredToolsKey];
4846

49-
if (!requestScopedDeferredToolNames) {
47+
if (!requestScopedDeferredTools) {
5048
throw new Error('ToolSearchTool: request-scoped deferred tools are unavailable. Ensure resolveInput is called before invoke.');
5149
}
5250

53-
const allowedToolNames = new Set(requestScopedDeferredToolNames);
54-
const availableTools = createRequestToolManifest(this._toolsService.tools, this._toolDeferralService)
55-
.deferredTools
56-
.filter(tool => allowedToolNames.has(tool.name));
5751
const matchedToolNames = await this._toolEmbeddingsComputer.searchToolsByQuery(
5852
query,
59-
availableTools,
53+
requestScopedDeferredTools,
6054
limit ?? DEFAULT_SEARCH_LIMIT,
6155
token,
6256
);
@@ -74,8 +68,8 @@ export class ToolSearchTool implements ICopilotModelSpecificTool<IToolSearchPara
7468
async resolveInput(input: IToolSearchParams, promptContext: IBuildPromptContext, _mode: CopilotToolMode): Promise<IToolSearchParams> {
7569
const manifest = createRequestToolManifest(promptContext.tools?.availableTools ?? [], this._toolDeferralService);
7670
const resolvedInput: ResolvedToolSearchParams = { ...input };
77-
Object.defineProperty(resolvedInput, requestScopedDeferredToolNamesKey, {
78-
value: Object.freeze([...manifest.deferredToolNames]),
71+
Object.defineProperty(resolvedInput, requestScopedDeferredToolsKey, {
72+
value: Object.freeze([...manifest.deferredTools]),
7973
enumerable: false,
8074
});
8175
return resolvedInput;

0 commit comments

Comments
 (0)