Fixes for tool invocations/confirmations to work better with external agents (#264180)

For #262756
This commit is contained in:
Rob Lourens
2025-08-30 14:42:07 -07:00
committed by GitHub
parent f4bea1d6c4
commit d668f8317b
11 changed files with 57 additions and 28 deletions
@@ -11,7 +11,7 @@ import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { revive } from '../../../base/common/marshalling.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/languageModelToolsService.js';
import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/editFileTool.js';
import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/tools.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
@@ -264,7 +264,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
} : undefined,
invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),
pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),
presentation: result.presentation
presentation: result.presentation as ToolInvocationPresentation | undefined
};
}
@@ -34,24 +34,28 @@ import * as languages from '../../../editor/common/languages.js';
import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js';
import { ITextEditorOptions } from '../../../platform/editor/common/editor.js';
import { IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js';
import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js';
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
import { IViewBadge } from '../../common/views.js';
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffData, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatVariableEntries.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffData, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatThinkingPart, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { IToolResult, IToolResultInputOutputDetails, IToolResultOutputDetails, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js';
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js';
import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
import * as notebooks from '../../contrib/notebook/common/notebookCommon.js';
import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js';
import { ICellRange } from '../../contrib/notebook/common/notebookRange.js';
import * as search from '../../contrib/search/common/search.js';
import { TestId } from '../../contrib/testing/common/testId.js';
import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestRunProfileReference, ITestTag, TestMessageType, TestResultItem, TestRunProfileBitset, denamespaceTestTag, namespaceTestTag } from '../../contrib/testing/common/testTypes.js';
import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js';
import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.js';
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
@@ -61,10 +65,6 @@ import { CommandsConverter } from './extHostCommands.js';
import { getPrivateApiFor } from './extHostTestingPrivateApi.js';
import * as types from './extHostTypes.js';
import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js';
import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
import { ILogService } from '../../../platform/log/common/log.js';
export namespace Command {
@@ -2784,7 +2784,7 @@ export namespace ChatPrepareToolInvocationPart {
}
export namespace ChatToolInvocationPart {
export function from(part: vscode.ChatToolInvocationPart): any {
export function from(part: vscode.ChatToolInvocationPart): IChatToolInvocationSerialized {
// Convert extension API ChatToolInvocationPart to internal serialized format
return {
kind: 'toolInvocationSerialized',
@@ -2795,8 +2795,8 @@ export namespace ChatToolInvocationPart {
pastTenseMessage: part.pastTenseMessage ? MarkdownString.from(part.pastTenseMessage) : undefined,
isConfirmed: part.isConfirmed,
isComplete: part.isComplete ?? true,
isError: part.isError ?? false,
resultDetails: undefined,
source: ToolDataSource.External,
// isError: part.isError ?? false,
toolSpecificData: part.toolSpecificData ? convertToolSpecificData(part.toolSpecificData) : undefined,
presentation: undefined
};
@@ -234,6 +234,8 @@ export async function showToolsPicker(
return BucketOrdinal.BuiltIn.toString();
case 'user':
return BucketOrdinal.User.toString();
case 'external':
throw new Error('should not be reachable');
default:
assertNever(source);
}
@@ -8,11 +8,12 @@ import { Emitter } from '../../../../../../base/common/event.js';
import { markdownCommandLink, MarkdownString } from '../../../../../../base/common/htmlContent.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js';
import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
import { localize } from '../../../../../../nls.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../common/chatService.js';
import { IChatRendererContent } from '../../../common/chatViewModel.js';
import { CodeBlockModelCollection } from '../../../common/codeBlockModelCollection.js';
import { isToolResultInputOutputDetails, isToolResultOutputDetails } from '../../../common/languageModelToolsService.js';
import { isToolResultInputOutputDetails, isToolResultOutputDetails, ToolInvocationPresentation } from '../../../common/languageModelToolsService.js';
import { ChatTreeItem, IChatCodeBlockInfo } from '../../chat.js';
import { IChatContentPart, IChatContentPartRenderContext } from '../chatContentParts.js';
import { EditorPool } from '../chatMarkdownContentPart.js';
@@ -20,14 +21,13 @@ import { CollapsibleListPool } from '../chatReferencesContentPart.js';
import { ExtensionsInstallConfirmationWidgetSubPart } from './chatExtensionsInstallToolSubPart.js';
import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownProgressPart.js';
import { ChatResultListSubPart } from './chatResultListSubPart.js';
import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js';
import { ChatTerminalToolConfirmationSubPart } from './chatTerminalToolConfirmationSubPart.js';
import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js';
import { ChatTodoListSubPart } from './chatTodoListSubPart.js';
import { ToolConfirmationSubPart } from './chatToolConfirmationSubPart.js';
import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js';
import { ChatToolOutputSubPart } from './chatToolOutputPart.js';
import { ChatToolProgressSubPart } from './chatToolProgressPart.js';
import { localize } from '../../../../../../nls.js';
import { ChatTodoListSubPart } from './chatTodoListSubPart.js';
export class ChatToolInvocationPart extends Disposable implements IChatContentPart {
public readonly domNode: HTMLElement;
@@ -71,6 +71,10 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
dom.clearNode(this.domNode);
partStore.clear();
if (toolInvocation.presentation === ToolInvocationPresentation.HiddenAfterComplete && toolInvocation.isComplete) {
return;
}
this.subPart = partStore.add(this.createToolInvocationSubPart());
this.domNode.appendChild(this.subPart.domNode);
partStore.add(this.subPart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
@@ -759,7 +759,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
if (
!lastPart ||
lastPart.kind === 'references' ||
(lastPart.kind === 'toolInvocation' && (lastPart.isComplete || lastPart.presentation === 'hidden')) ||
((lastPart.kind === 'toolInvocation' || lastPart.kind === 'toolInvocationSerialized') && (lastPart.isComplete || lastPart.presentation === 'hidden')) ||
((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !part.isComplete)) ||
(lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) ||
lastPart.kind === 'prepareToolInvocation'
@@ -758,7 +758,7 @@ class BuiltinDynamicCompletions extends Disposable {
// If locked to an agent that doesn't support file attachments, skip
if (widget.lockedAgentId) {
const agent = this.chatAgentService.getAgent(widget.lockedAgentId);
if (agent && agent.capabilities && agent.capabilities.supportsFileAttachments === false) {
if (agent && !agent.capabilities?.supportsFileAttachments) {
return result;
}
}
@@ -1125,7 +1125,7 @@ class ToolCompletions extends Disposable {
// If locked to an agent that doesn't support tool attachments, skip
if (widget.lockedAgentId) {
const agent = this.chatAgentService.getAgent(widget.lockedAgentId);
if (agent && agent.capabilities?.supportsToolAttachments === false) {
if (agent && !agent.capabilities?.supportsToolAttachments) {
return null;
}
}
@@ -967,7 +967,17 @@ export class ChatService extends Disposable implements IChatService {
} satisfies IChatAgentRequest;
};
if (this.configurationService.getValue('chat.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && !agentSlashCommandPart && enableCommandDetection && options?.modeInfo?.kind !== ChatModeKind.Agent && options?.modeInfo?.kind !== ChatModeKind.Edit) {
if (
this.configurationService.getValue('chat.detectParticipant.enabled') !== false &&
this.chatAgentService.hasChatParticipantDetectionProviders() &&
!agentPart &&
!commandPart &&
!agentSlashCommandPart &&
enableCommandDetection &&
options?.modeInfo?.kind !== ChatModeKind.Agent &&
options?.modeInfo?.kind !== ChatModeKind.Edit &&
!options?.agentIdSilent
) {
// We have no agent or command to scope history with, pass the full history to the participant detection provider
const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id);
@@ -75,18 +75,25 @@ export type ToolDataSource =
| {
type: 'internal';
label: string;
} | {
type: 'external';
label: string;
};
export namespace ToolDataSource {
export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' };
/** External tools may not be contributed or invoked, but may be invoked externally and described in an IChatToolInvocationSerialized */
export const External: ToolDataSource = { type: 'external', label: 'External' };
export function toKey(source: ToolDataSource): string {
switch (source.type) {
case 'extension': return `extension:${source.extensionId.value}`;
case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`;
case 'user': return `user:${source.file.toString()}`;
case 'internal': return 'internal';
case 'external': return 'external';
}
}
@@ -222,12 +229,17 @@ export interface IToolConfirmationAction {
export type ToolConfirmationAction = IToolConfirmationAction | Separator;
export enum ToolInvocationPresentation {
Hidden = 'hidden',
HiddenAfterComplete = 'hiddenAfterComplete'
}
export interface IPreparedToolInvocation {
invocationMessage?: string | IMarkdownString;
pastTenseMessage?: string | IMarkdownString;
originMessage?: string | IMarkdownString;
confirmationMessages?: IToolConfirmationMessages;
presentation?: 'hidden' | undefined;
presentation?: ToolInvocationPresentation;
toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent;
}
@@ -6,7 +6,7 @@
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
import { IChatTerminalToolInvocationData } from '../chatService.js';
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../languageModelToolsService.js';
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js';
export const ConfirmationToolId = 'vscode_get_confirmation';
@@ -41,7 +41,7 @@ export const ConfirmationToolData: IToolData = {
}
};
export interface ConfirmationToolParams {
export interface IConfirmationToolParams {
title: string;
message: string;
confirmationType?: 'basic' | 'terminal';
@@ -50,7 +50,7 @@ export interface ConfirmationToolParams {
export class ConfirmationTool implements IToolImpl {
async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
const parameters = context.parameters as ConfirmationToolParams;
const parameters = context.parameters as IConfirmationToolParams;
if (!parameters.title || !parameters.message) {
throw new Error('Missing required parameters for ConfirmationTool');
}
@@ -80,7 +80,8 @@ export class ConfirmationTool implements IToolImpl {
message: new MarkdownString(parameters.message),
allowAutoConfirm: false
},
toolSpecificData
toolSpecificData,
presentation: ToolInvocationPresentation.HiddenAfterComplete
};
}
@@ -89,7 +90,7 @@ export class ConfirmationTool implements IToolImpl {
return {
content: [{
kind: 'text',
value: `Confirmed`
value: 'yes' // Consumers should check for this label to know whether the tool was confirmed or skipped
}]
};
}
@@ -13,7 +13,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js';
import { ICodeMapperService } from '../../common/chatCodeMapperService.js';
import { ChatModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js';
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../common/languageModelToolsService.js';
export const ExtensionEditToolId = 'vscode_editFile';
export const InternalEditToolId = 'vscode_editFile_internal';
@@ -138,7 +138,7 @@ export class EditTool implements IToolImpl {
async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
return {
presentation: 'hidden'
presentation: ToolInvocationPresentation.Hidden
};
}
}
@@ -24,7 +24,7 @@ import { TerminalCapability } from '../../../../../../platform/terminal/common/c
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js';
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress, type IToolConfirmationMessages, type ToolConfirmationAction } from '../../../../chat/common/languageModelToolsService.js';
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress, type IToolConfirmationMessages, type ToolConfirmationAction } from '../../../../chat/common/languageModelToolsService.js';
import { ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js';
import type { XtermTerminal } from '../../../../terminal/browser/xterm/xtermTerminal.js';
import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js';
@@ -211,7 +211,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
const args = context.parameters as IRunInTerminalInputParams;
const alternativeRecommendation = getRecommendedToolsOverRunInTerminal(args.command, this._languageModelToolsService);
const presentation = alternativeRecommendation ? 'hidden' : undefined;
const presentation = alternativeRecommendation ? ToolInvocationPresentation.Hidden : undefined;
const os = await this._osBackend;
const shell = await this._getCopilotShell();