diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 252f57d854e..b8f4106fc97 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -163,7 +163,7 @@ "localization": { "description": { "key": "chat.agent.enabled.description", - "value": "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view." + "value": "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used." } }, "type": "boolean", diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4cc68945e42..b9cf7681dd5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -538,7 +538,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.AgentEnabled]: { type: 'boolean', - description: nls.localize('chat.agent.enabled.description', "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view."), + description: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."), default: true, policy: { name: 'ChatAgentMode', @@ -548,7 +548,7 @@ configurationRegistry.registerConfiguration({ localization: { description: { key: 'chat.agent.enabled.description', - value: nls.localize('chat.agent.enabled.description', "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view."), + value: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."), } } } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 8823457d37c..bc823040c7a 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -16,7 +16,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { derived, IObservable, observableFromEventOpts, ObservableSet } from '../../../../base/common/observable.js'; +import { derived, IObservable, IReader, observableFromEventOpts, ObservableSet } from '../../../../base/common/observable.js'; import Severity from '../../../../base/common/severity.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -29,6 +29,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -93,6 +94,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _callsByRequestId = new Map(); + private readonly _isAgentModeEnabled: IObservable; + constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IExtensionService private readonly _extensionService: IExtensionService, @@ -109,6 +112,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo ) { super(); + this._isAgentModeEnabled = observableConfigValue(ChatConfiguration.AgentEnabled, true, this._configurationService); + this._register(this._contextKeyService.onDidChangeContext(e => { if (e.affectsSome(this._toolContextKeys)) { // Not worth it to compute a delta here unless we have many tools changing often @@ -117,7 +122,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo })); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled)) { + if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled) || e.affectsConfiguration(ChatConfiguration.AgentEnabled)) { this._onDidChangeToolsScheduler.schedule(); } })); @@ -166,6 +171,27 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } )); } + + /** + * Returns if the given tool or toolset is permitted in the current context. + * When agent mode is enabled, all tools are permitted (no restriction) + * When agent mode is disabled only a subset of read-only tools are permitted in agentic-loop contexts. + */ + private isPermitted(toolOrToolSet: IToolData | ToolSet, reader?: IReader): boolean { + const agentModeEnabled = this._isAgentModeEnabled.read(reader); + if (agentModeEnabled !== false) { + return true; + } + const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web]; + if (toolOrToolSet instanceof ToolSet) { + const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName); + this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`); + return permitted; + } + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`); + return false; + } + override dispose(): void { super.dispose(); @@ -243,7 +269,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo toolData => { const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when); const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled; - return satisfiesWhenClause && satisfiesExternalToolCheck; + const satisfiesPermittedCheck = includeDisabled || this.isPermitted(toolData); + return satisfiesWhenClause && satisfiesExternalToolCheck && satisfiesPermittedCheck; }); } @@ -853,7 +880,10 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _toolSets = new ObservableSet(); - readonly toolSets: IObservable> = this._toolSets.observable; + readonly toolSets: IObservable> = derived(this, reader => { + const allToolSets = Array.from(this._toolSets.observable.read(reader)); + return allToolSets.filter(toolSet => this.isPermitted(toolSet, reader)); + }); getToolSet(id: string): ToolSet | undefined { for (const toolSet of this._toolSets) { @@ -916,7 +946,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } for (const tool of this.toolsObservable.read(reader)) { - if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool)) { + if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool) && this.isPermitted(tool, reader)) { result.push([tool, getToolFullReferenceName(tool)]); } }