Path: blob/main/extensions/copilot/src/extension/intents/node/hookResultProcessor.ts
13399 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 * as l10n from '@vscode/l10n';6import type { ChatResponseStream } from 'vscode';7import { ILogService } from '../../../platform/log/common/logService';8import { ChatHookType } from '../../../vscodeTypes';910/**11* Error thrown when a hook requests the agent to abort processing.12* The message should be shown to the user.13*/14export class HookAbortError extends Error {15constructor(16public readonly hookType: string,17public readonly stopReason: string18) {19super(`Hook ${hookType} aborted: ${stopReason}`);20this.name = 'HookAbortError';21}22}2324/**25* Type guard to check if an error is a HookAbortError.26*/27export function isHookAbortError(error: unknown): error is HookAbortError {28return error instanceof HookAbortError;29}3031/**32* A hook result from the chat hook service.33*/34export interface HookResult {35stopReason?: string;36resultKind: 'success' | 'error' | 'warning';37warningMessage?: string;38output: unknown;39}4041/**42* Options for processing hook results.43*/44export interface ProcessHookResultsOptions {45/** The type of hook being processed */46hookType: ChatHookType;47/** The hook results to process */48results: readonly HookResult[];49/** The output stream for displaying messages */50outputStream: ChatResponseStream | undefined;51/** The log service for logging */52logService: ILogService;53/** Callback for handling successful hook results. Called with the output for each success. */54onSuccess: (output: unknown) => void;55/**56* When true, errors and stopReason are completely ignored (no throw, no warning, no hookProgress).57* Use for hooks like SessionStart/SubagentStart where blocking errors should be silently ignored.58*/59ignoreErrors?: boolean;60/**61* Callback for handling error results. When provided, errors are passed to this callback62* instead of being shown to the user. Use for Stop/SubagentStop hooks where errors63* should be collected as blocking reasons.64*/65onError?: (errorMessage: string) => void;66}6768/**69* Processes hook results, handling aborts, warnings, errors, and success cases.70* Warnings are aggregated and displayed together via hookProgress after processing all results.71*72* @param options The processing options73* @throws HookAbortError if any result contains a stopReason or an error result is encountered74*/75export function processHookResults(options: ProcessHookResultsOptions): void {76const { hookType, results, outputStream, logService, onSuccess, ignoreErrors, onError } = options;7778const warnings: string[] = [];7980for (const result of results) {81// Check for stopReason - abort immediately (unless ignoreErrors is set)82// Note: empty string is a valid stopReason (from continue: false without explicit message)83if (result.stopReason !== undefined) {84if (ignoreErrors) {85logService.trace(`[ToolCallingLoop] ${hookType} hook stopReason ignored: ${result.stopReason}`);86continue;87}88logService.info(`[ToolCallingLoop] ${hookType} hook requested abort: ${result.stopReason}`);89outputStream?.hookProgress(hookType, formatHookErrorMessage(result.stopReason));90throw new HookAbortError(hookType, result.stopReason);91}9293// Collect warnings94if (result.resultKind === 'warning' && result.warningMessage) {95logService.trace(`[ToolCallingLoop] ${hookType} hook warning: ${result.warningMessage}`);96warnings.push(result.warningMessage);97}9899// Handle success100if (result.resultKind === 'success') {101if (result.warningMessage) {102warnings.push(result.warningMessage);103}104onSuccess(result.output);105}106107// Handle error - abort unless ignoreErrors is set or onError is provided108if (result.resultKind === 'error') {109const errorMessage = typeof result.output === 'string' && result.output ? result.output : '';110logService.error(new Error(errorMessage), `[ToolCallingLoop] ${hookType} hook error`);111if (onError) {112// Pass error to callback (for Stop/SubagentStop to collect as blocking reason)113onError(errorMessage);114continue;115} else if (ignoreErrors) {116// Completely ignore error - no throw, no hookProgress (silently continue)117continue;118} else {119outputStream?.hookProgress(hookType, formatHookErrorMessage(errorMessage));120throw new HookAbortError(hookType, errorMessage);121}122}123}124125// Show aggregated warnings via hookProgress126if (warnings.length > 0 && outputStream) {127if (warnings.length === 1) {128outputStream.hookProgress(hookType, undefined, warnings[0]);129} else {130const formattedWarnings = warnings.map((w, i) => `${i + 1}. ${w}`).join('\n');131outputStream.hookProgress(hookType, undefined, formattedWarnings);132}133}134}135136/**137* Formats a localized error message for a failed hook.138* @param errorMessage The error message from the hook139* @returns A localized error message string140*/141export function formatHookErrorMessage(errorMessage: string): string {142if (errorMessage) {143return l10n.t('A hook prevented chat from continuing. Please check the GitHub Copilot Chat Hooks output channel for more details. \nError message: {0}', errorMessage);144}145return l10n.t('A hook prevented chat from continuing. Please check the GitHub Copilot Chat Hooks output channel for more details.');146}147148149