Skip to content

Commit 8a0f412

Browse files
authored
check tool specific enablement when reading tool set (microsoft#250984)
fixes microsoft/vscode-copilot#18161
1 parent f832c6b commit 8a0f412

File tree

2 files changed

+118
-2
lines changed

2 files changed

+118
-2
lines changed

src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export class ChatSelectedTools extends Disposable {
151151

152152
asEnablementMap(): Map<IToolData, boolean> {
153153
const result = new Map<IToolData, boolean>();
154+
const map = this.entriesMap;
154155

155156
const _set = (tool: IToolData, enabled: boolean) => {
156157
// ONLY disable a tool that isn't enabled yet
@@ -160,10 +161,10 @@ export class ChatSelectedTools extends Disposable {
160161
}
161162
};
162163

163-
for (const [item, enabled] of this.entriesMap) {
164+
for (const [item, enabled] of map) {
164165
if (item instanceof ToolSet) {
165166
for (const tool of item.getTools()) {
166-
_set(tool, enabled);
167+
_set(tool, map.get(tool) ?? enabled); // tools from tool set can be explicitly set
167168
}
168169
} else {
169170
if (item.canBeReferencedInPrompt) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import assert from 'assert';
7+
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
8+
import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js';
9+
import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
10+
import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js';
11+
import { IChatService } from '../../common/chatService.js';
12+
import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../common/languageModelToolsService.js';
13+
import { MockChatService } from '../common/mockChatService.js';
14+
import { ChatSelectedTools } from '../../browser/chatSelectedTools.js';
15+
import { constObservable } from '../../../../../base/common/observable.js';
16+
import { ChatMode } from '../../common/constants.js';
17+
import { Iterable } from '../../../../../base/common/iterator.js';
18+
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
19+
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
20+
import { timeout } from '../../../../../base/common/async.js';
21+
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
22+
23+
suite('ChatSelectedTools', () => {
24+
25+
let store: DisposableStore;
26+
27+
let toolsService: ILanguageModelToolsService;
28+
let selectedTools: ChatSelectedTools;
29+
30+
setup(() => {
31+
32+
store = new DisposableStore();
33+
34+
const instaService = workbenchInstantiationService({
35+
contextKeyService: () => store.add(new ContextKeyService(new TestConfigurationService)),
36+
}, store);
37+
instaService.stub(IChatService, new MockChatService());
38+
instaService.stub(ILanguageModelToolsService, instaService.createInstance(LanguageModelToolsService));
39+
40+
store.add(instaService);
41+
toolsService = instaService.get(ILanguageModelToolsService);
42+
selectedTools = store.add(instaService.createInstance(ChatSelectedTools, constObservable(ChatMode.Agent)));
43+
});
44+
45+
teardown(function () {
46+
store.dispose();
47+
});
48+
49+
ensureNoDisposablesAreLeakedInTestSuite();
50+
51+
test('Can\'t enable/disable MCP tools directly #18161', () => {
52+
53+
return runWithFakedTimers({}, async () => {
54+
55+
const toolData1: IToolData = {
56+
id: 'testTool1',
57+
modelDescription: 'Test Tool 1',
58+
displayName: 'Test Tool 1',
59+
canBeReferencedInPrompt: true,
60+
toolReferenceName: 't1',
61+
source: ToolDataSource.Internal,
62+
};
63+
64+
const toolData2: IToolData = {
65+
id: 'testTool2',
66+
modelDescription: 'Test Tool 2',
67+
displayName: 'Test Tool 2',
68+
source: ToolDataSource.Internal,
69+
canBeReferencedInPrompt: true,
70+
toolReferenceName: 't2',
71+
};
72+
73+
const toolData3: IToolData = {
74+
id: 'testTool3',
75+
modelDescription: 'Test Tool 3',
76+
displayName: 'Test Tool 3',
77+
source: ToolDataSource.Internal,
78+
canBeReferencedInPrompt: true,
79+
toolReferenceName: 't3',
80+
};
81+
82+
const toolset = toolsService.createToolSet(
83+
ToolDataSource.Internal,
84+
'mcp', 'mcp'
85+
);
86+
87+
store.add(toolsService.registerToolData(toolData1));
88+
store.add(toolsService.registerToolData(toolData2));
89+
store.add(toolsService.registerToolData(toolData3));
90+
91+
store.add(toolset);
92+
store.add(toolset.addTool(toolData1));
93+
store.add(toolset.addTool(toolData2));
94+
store.add(toolset.addTool(toolData3));
95+
96+
assert.strictEqual(Iterable.length(toolsService.getTools()), 3);
97+
98+
const size = Iterable.length(toolset.getTools());
99+
assert.strictEqual(size, 3);
100+
101+
await timeout(1000); // UGLY the tools service updates its state sync but emits the event async (750ms) delay. This affects the observable that depends on the event
102+
103+
assert.strictEqual(selectedTools.entriesMap.size, 4); // 1 toolset, 3 tools
104+
105+
selectedTools.disable([], [toolData2, toolData3], false);
106+
107+
const map = selectedTools.asEnablementMap();
108+
assert.strictEqual(map.size, 3); // 3 tools
109+
110+
assert.strictEqual(map.get(toolData1), true);
111+
assert.strictEqual(map.get(toolData2), false);
112+
assert.strictEqual(map.get(toolData3), false);
113+
});
114+
});
115+
});

0 commit comments

Comments
 (0)