@@ -10,6 +10,8 @@ import { IToolDeferralService } from '../../../../platform/networking/common/too
1010import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation' ;
1111import { createExtensionUnitTestingServices } from '../../../test/node/services' ;
1212import { TestChatRequest } from '../../../test/node/testHelpers' ;
13+ import { ChatVariablesCollection } from '../../../prompt/common/chatVariablesCollection' ;
14+ import { IBuildPromptContext } from '../../../prompt/common/intents' ;
1315import { IToolsService } from '../../common/toolsService' ;
1416import { ToolSearchTool } from '../toolSearchTool' ;
1517import { TestToolsService } from './testToolsService' ;
@@ -24,6 +26,19 @@ function makeToolInfo(name: string): vscode.LanguageModelToolInformation {
2426 } as vscode . LanguageModelToolInformation ;
2527}
2628
29+ function makePromptContext ( availableTools ?: readonly vscode . LanguageModelToolInformation [ ] ) : IBuildPromptContext {
30+ return {
31+ query : 'test query' ,
32+ history : [ ] ,
33+ chatVariables : new ChatVariablesCollection ( [ ] ) ,
34+ tools : availableTools ? {
35+ toolReferences : [ ] ,
36+ toolInvocationToken : undefined as never ,
37+ availableTools,
38+ } : undefined ,
39+ } ;
40+ }
41+
2742suite ( 'ToolSearchTool' , ( ) => {
2843 test ( 'searches only deferred tools enabled for the active request' , async ( ) => {
2944 const searchedToolNames : string [ ] [ ] = [ ] ;
@@ -157,6 +172,77 @@ suite('ToolSearchTool', () => {
157172 expect ( searchedToolNames ) . toEqual ( [ [ 'activate_vs_code_interaction' ] ] ) ;
158173 } ) ;
159174
175+ test ( 'preserves request-scoped deferred tools after resolved input is shallow-cloned' , async ( ) => {
176+ const searchedToolNames : string [ ] [ ] = [ ] ;
177+ const nonDeferred = new Set ( [ 'read_file' ] ) ;
178+ const tool = new ToolSearchTool (
179+ {
180+ searchToolsByQuery : async ( _query : string , tools : readonly vscode . LanguageModelToolInformation [ ] ) => {
181+ searchedToolNames . push ( tools . map ( candidate => candidate . name ) ) ;
182+ return tools . map ( candidate => candidate . name ) ;
183+ } ,
184+ } as any ,
185+ {
186+ isNonDeferredTool : ( name : string ) => nonDeferred . has ( name ) ,
187+ } as any ,
188+ { trace ( ) { } } as any ,
189+ ) ;
190+
191+ const resolvedInput = await tool . resolveInput (
192+ { query : 'vscode interaction tools' } ,
193+ makePromptContext ( [
194+ makeToolInfo ( 'read_file' ) ,
195+ makeToolInfo ( 'activate_vs_code_interaction' ) ,
196+ ] ) ,
197+ 0 ,
198+ ) ;
199+
200+ const clonedResolvedInput = { ...resolvedInput } ;
201+
202+ await tool . invoke (
203+ { input : clonedResolvedInput } as vscode . LanguageModelToolInvocationOptions < { query : string } > ,
204+ { isCancellationRequested : false } as vscode . CancellationToken ,
205+ ) ;
206+
207+ expect ( searchedToolNames ) . toEqual ( [ [ 'activate_vs_code_interaction' ] ] ) ;
208+ expect ( ( tool as any ) . _requestScopedDeferredToolsContexts . size ) . toBe ( 0 ) ;
209+ } ) ;
210+
211+ test ( 'preserves request-scoped deferred tools after resolved input is JSON-round-tripped' , async ( ) => {
212+ const searchedToolNames : string [ ] [ ] = [ ] ;
213+ const nonDeferred = new Set ( [ 'read_file' ] ) ;
214+ const tool = new ToolSearchTool (
215+ {
216+ searchToolsByQuery : async ( _query : string , tools : readonly vscode . LanguageModelToolInformation [ ] ) => {
217+ searchedToolNames . push ( tools . map ( candidate => candidate . name ) ) ;
218+ return tools . map ( candidate => candidate . name ) ;
219+ } ,
220+ } as any ,
221+ {
222+ isNonDeferredTool : ( name : string ) => nonDeferred . has ( name ) ,
223+ } as any ,
224+ { trace ( ) { } } as any ,
225+ ) ;
226+
227+ const resolvedInput = await tool . resolveInput (
228+ { query : 'vscode interaction tools' } ,
229+ makePromptContext ( [
230+ makeToolInfo ( 'read_file' ) ,
231+ makeToolInfo ( 'activate_vs_code_interaction' ) ,
232+ ] ) ,
233+ 0 ,
234+ ) ;
235+
236+ const jsonRoundTrippedInput = JSON . parse ( JSON . stringify ( resolvedInput ) ) ;
237+
238+ await tool . invoke (
239+ { input : jsonRoundTrippedInput } as vscode . LanguageModelToolInvocationOptions < { query : string } > ,
240+ { isCancellationRequested : false } as vscode . CancellationToken ,
241+ ) ;
242+
243+ expect ( searchedToolNames ) . toEqual ( [ [ 'activate_vs_code_interaction' ] ] ) ;
244+ } ) ;
245+
160246 test ( 'fails explicitly when invoke runs without request-scoped resolveInput context' , async ( ) => {
161247 const nonDeferred = new Set ( [ 'read_file' ] ) ;
162248 const tool = new ToolSearchTool (
0 commit comments