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