Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { Disposable } from '../../../../base/common/lifecycle.js';7import { derived, IObservable, observableFromEvent, ObservableMap } from '../../../../base/common/observable.js';8import { isObject } from '../../../../base/common/types.js';9import { URI } from '../../../../base/common/uri.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js';12import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';13import { UserSelectedTools } from '../common/chatAgents.js';14import { IChatMode } from '../common/chatModes.js';15import { ChatModeKind } from '../common/constants.js';16import { ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, ToolSet } from '../common/languageModelToolsService.js';17import { PromptFileRewriter } from './promptSyntax/promptFileRewriter.js';181920type ToolEnablementStates = {21readonly toolSets: ReadonlyMap<string, boolean>;22readonly tools: ReadonlyMap<string, boolean>;23};2425type StoredDataV2 = {26readonly version: 2;27readonly toolSetEntries: [string, boolean][];28readonly toolEntries: [string, boolean][];29};3031type StoredDataV1 = {32readonly version: undefined;33readonly disabledToolSets?: string[];34readonly disabledTools?: string[];35};3637namespace ToolEnablementStates {38export function fromMap(map: IToolAndToolSetEnablementMap): ToolEnablementStates {39const toolSets: Map<string, boolean> = new Map(), tools: Map<string, boolean> = new Map();40for (const [entry, enabled] of map.entries()) {41if (entry instanceof ToolSet) {42toolSets.set(entry.id, enabled);43} else {44tools.set(entry.id, enabled);45}46}47return { toolSets, tools };48}4950function isStoredDataV1(data: StoredDataV1 | StoredDataV2 | undefined): data is StoredDataV1 {51return isObject(data) && data.version === undefined52&& (data.disabledTools === undefined || Array.isArray(data.disabledTools))53&& (data.disabledToolSets === undefined || Array.isArray(data.disabledToolSets));54}5556function isStoredDataV2(data: StoredDataV1 | StoredDataV2 | undefined): data is StoredDataV2 {57return isObject(data) && data.version === 2 && Array.isArray(data.toolSetEntries) && Array.isArray(data.toolEntries);58}5960export function fromStorage(storage: string): ToolEnablementStates {61try {62const parsed = JSON.parse(storage);63if (isStoredDataV2(parsed)) {64return { toolSets: new Map(parsed.toolSetEntries), tools: new Map(parsed.toolEntries) };65} else if (isStoredDataV1(parsed)) {66const toolSetEntries = parsed.disabledToolSets?.map(id => [id, false] as [string, boolean]);67const toolEntries = parsed.disabledTools?.map(id => [id, false] as [string, boolean]);68return { toolSets: new Map(toolSetEntries), tools: new Map(toolEntries) };69}70} catch {71// ignore72}73// invalid data74return { toolSets: new Map(), tools: new Map() };75}7677export function toStorage(state: ToolEnablementStates): string {78const storageData: StoredDataV2 = {79version: 2,80toolSetEntries: Array.from(state.toolSets.entries()),81toolEntries: Array.from(state.tools.entries())82};83return JSON.stringify(storageData);84}85}8687export enum ToolsScope {88Global,89Session,90Mode91}9293export class ChatSelectedTools extends Disposable {9495private readonly _globalState: ObservableMemento<ToolEnablementStates>;9697private readonly _sessionStates = new ObservableMap<string, ToolEnablementStates | undefined>();9899private readonly _allTools: IObservable<Readonly<IToolData>[]>;100101constructor(102private readonly _mode: IObservable<IChatMode>,103@ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService,104@IStorageService _storageService: IStorageService,105@IInstantiationService private readonly _instantiationService: IInstantiationService,106) {107super();108109const globalStateMemento = observableMemento<ToolEnablementStates>({110key: 'chat/selectedTools',111defaultValue: { toolSets: new Map(), tools: new Map() },112fromStorage: ToolEnablementStates.fromStorage,113toStorage: ToolEnablementStates.toStorage114});115116this._globalState = this._store.add(globalStateMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE, _storageService));117this._allTools = observableFromEvent(_toolsService.onDidChangeTools, () => Array.from(_toolsService.getTools()));118}119120/**121* All tools and tool sets with their enabled state.122*/123public readonly entriesMap: IObservable<IToolAndToolSetEnablementMap> = derived(r => {124const map = new Map<IToolData | ToolSet, boolean>();125126// look up the tools in the hierarchy: session > mode > global127const currentMode = this._mode.read(r);128let currentMap = this._sessionStates.observable.read(r).get(currentMode.id);129if (!currentMap && currentMode.kind === ChatModeKind.Agent) {130const modeTools = currentMode.customTools?.read(r);131if (modeTools) {132currentMap = ToolEnablementStates.fromMap(this._toolsService.toToolAndToolSetEnablementMap(modeTools));133}134}135if (!currentMap) {136currentMap = this._globalState.read(r);137}138for (const tool of this._allTools.read(r)) {139if (tool.canBeReferencedInPrompt) {140map.set(tool, currentMap.tools.get(tool.id) !== false); // if unknown, it's enabled141}142}143for (const toolSet of this._toolsService.toolSets.read(r)) {144const toolSetEnabled = currentMap.toolSets.get(toolSet.id) !== false; // if unknown, it's enabled145map.set(toolSet, toolSetEnabled);146for (const tool of toolSet.getTools(r)) {147map.set(tool, toolSetEnabled || currentMap.tools.get(tool.id) === true); // if unknown, use toolSetEnabled148}149}150return map;151});152153public readonly userSelectedTools: IObservable<UserSelectedTools> = derived(r => {154// extract a map of tool ids155const result: UserSelectedTools = {};156const map = this.entriesMap.read(r);157for (const [item, enabled] of map) {158if (!(item instanceof ToolSet)) {159result[item.id] = enabled;160}161}162return result;163});164165get entriesScope() {166const mode = this._mode.get();167if (this._sessionStates.has(mode.id)) {168return ToolsScope.Session;169}170if (mode.kind === ChatModeKind.Agent && mode.customTools?.get() && mode.uri) {171return ToolsScope.Mode;172}173return ToolsScope.Global;174}175176get currentMode(): IChatMode {177return this._mode.get();178}179180resetSessionEnablementState() {181const mode = this._mode.get();182this._sessionStates.delete(mode.id);183}184185set(enablementMap: IToolAndToolSetEnablementMap, sessionOnly: boolean): void {186const mode = this._mode.get();187if (sessionOnly || this._sessionStates.has(mode.id)) {188this._sessionStates.set(mode.id, ToolEnablementStates.fromMap(enablementMap));189return;190}191if (mode.kind === ChatModeKind.Agent && mode.customTools?.get() && mode.uri) {192// apply directly to mode file.193this.updateCustomModeTools(mode.uri.get(), enablementMap);194return;195}196this._globalState.set(ToolEnablementStates.fromMap(enablementMap), undefined);197}198199private async updateCustomModeTools(uri: URI, enablementMap: IToolAndToolSetEnablementMap): Promise<void> {200await this._instantiationService.createInstance(PromptFileRewriter).openAndRewriteTools(uri, enablementMap, CancellationToken.None);201}202}203204205