mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
add singal abort for hooks
This commit is contained in:
parent
a0041191a7
commit
8bd7cf2cda
16 changed files with 344 additions and 52 deletions
|
|
@ -41,6 +41,7 @@ import {
|
||||||
Storage,
|
Storage,
|
||||||
SessionEndReason,
|
SessionEndReason,
|
||||||
SessionStartSource,
|
SessionStartSource,
|
||||||
|
type PermissionMode,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { buildResumedHistoryItems } from './utils/resumeHistoryUtils.js';
|
import { buildResumedHistoryItems } from './utils/resumeHistoryUtils.js';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
import { validateAuthMethod } from '../config/auth.js';
|
||||||
|
|
@ -308,7 +309,11 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||||
|
|
||||||
if (hookSystem) {
|
if (hookSystem) {
|
||||||
hookSystem
|
hookSystem
|
||||||
.fireSessionStartEvent(sessionStartSource, config.getModel() ?? '')
|
.fireSessionStartEvent(
|
||||||
|
sessionStartSource,
|
||||||
|
config.getModel() ?? '',
|
||||||
|
String(config.getApprovalMode()) as PermissionMode,
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
debugLogger.debug('SessionStart event completed successfully');
|
debugLogger.debug('SessionStart event completed successfully');
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ describe('clearCommand', () => {
|
||||||
}),
|
}),
|
||||||
getModel: () => 'test-model',
|
getModel: () => 'test-model',
|
||||||
getToolRegistry: () => undefined,
|
getToolRegistry: () => undefined,
|
||||||
|
getApprovalMode: () => 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
|
|
@ -108,6 +109,7 @@ describe('clearCommand', () => {
|
||||||
expect(mockFireSessionStartEvent).toHaveBeenCalledWith(
|
expect(mockFireSessionStartEvent).toHaveBeenCalledWith(
|
||||||
SessionStartSource.Clear,
|
SessionStartSource.Clear,
|
||||||
'test-model',
|
'test-model',
|
||||||
|
expect.any(String), // permissionMode
|
||||||
);
|
);
|
||||||
|
|
||||||
// SessionEnd should be called before SessionStart
|
// SessionEnd should be called before SessionStart
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
SessionStartSource,
|
SessionStartSource,
|
||||||
ToolNames,
|
ToolNames,
|
||||||
SkillTool,
|
SkillTool,
|
||||||
|
type PermissionMode,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
export const clearCommand: SlashCommand = {
|
export const clearCommand: SlashCommand = {
|
||||||
|
|
@ -72,6 +73,7 @@ export const clearCommand: SlashCommand = {
|
||||||
?.fireSessionStartEvent(
|
?.fireSessionStartEvent(
|
||||||
SessionStartSource.Clear,
|
SessionStartSource.Clear,
|
||||||
config.getModel() ?? '',
|
config.getModel() ?? '',
|
||||||
|
String(config.getApprovalMode()) as PermissionMode,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
SessionService,
|
SessionService,
|
||||||
type Config,
|
type Config,
|
||||||
SessionStartSource,
|
SessionStartSource,
|
||||||
|
type PermissionMode,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { buildResumedHistoryItems } from '../utils/resumeHistoryUtils.js';
|
import { buildResumedHistoryItems } from '../utils/resumeHistoryUtils.js';
|
||||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
|
|
@ -78,6 +79,7 @@ export function useResumeCommand(
|
||||||
?.fireSessionStartEvent(
|
?.fireSessionStartEvent(
|
||||||
SessionStartSource.Resume,
|
SessionStartSource.Resume,
|
||||||
config.getModel() ?? '',
|
config.getModel() ?? '',
|
||||||
|
String(config.getApprovalMode()) as PermissionMode,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
||||||
|
|
|
||||||
|
|
@ -810,19 +810,33 @@ export class Config {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if request was aborted
|
||||||
|
if (request.signal?.aborted) {
|
||||||
|
this.messageBus?.publish({
|
||||||
|
type: MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
|
correlationId: request.correlationId,
|
||||||
|
success: false,
|
||||||
|
error: new Error('Hook execution cancelled (aborted)'),
|
||||||
|
} as HookExecutionResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the appropriate hook based on eventName
|
// Execute the appropriate hook based on eventName
|
||||||
let result;
|
let result;
|
||||||
const input = request.input || {};
|
const input = request.input || {};
|
||||||
|
const signal = request.signal;
|
||||||
switch (request.eventName) {
|
switch (request.eventName) {
|
||||||
case 'UserPromptSubmit':
|
case 'UserPromptSubmit':
|
||||||
result = await hookSystem.fireUserPromptSubmitEvent(
|
result = await hookSystem.fireUserPromptSubmitEvent(
|
||||||
(input['prompt'] as string) || '',
|
(input['prompt'] as string) || '',
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'Stop':
|
case 'Stop':
|
||||||
result = await hookSystem.fireStopEvent(
|
result = await hookSystem.fireStopEvent(
|
||||||
(input['stop_hook_active'] as boolean) || false,
|
(input['stop_hook_active'] as boolean) || false,
|
||||||
(input['last_assistant_message'] as string) || '',
|
(input['last_assistant_message'] as string) || '',
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'PreToolUse': {
|
case 'PreToolUse': {
|
||||||
|
|
@ -832,6 +846,7 @@ export class Config {
|
||||||
(input['tool_use_id'] as string) || '',
|
(input['tool_use_id'] as string) || '',
|
||||||
(input['permission_mode'] as PermissionMode | undefined) ??
|
(input['permission_mode'] as PermissionMode | undefined) ??
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -842,6 +857,7 @@ export class Config {
|
||||||
(input['tool_response'] as Record<string, unknown>) || {},
|
(input['tool_response'] as Record<string, unknown>) || {},
|
||||||
(input['tool_use_id'] as string) || '',
|
(input['tool_use_id'] as string) || '',
|
||||||
(input['permission_mode'] as PermissionMode) || 'default',
|
(input['permission_mode'] as PermissionMode) || 'default',
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'PostToolUseFailure':
|
case 'PostToolUseFailure':
|
||||||
|
|
@ -852,6 +868,7 @@ export class Config {
|
||||||
(input['error'] as string) || '',
|
(input['error'] as string) || '',
|
||||||
input['is_interrupt'] as boolean | undefined,
|
input['is_interrupt'] as boolean | undefined,
|
||||||
(input['permission_mode'] as PermissionMode) || 'default',
|
(input['permission_mode'] as PermissionMode) || 'default',
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'Notification':
|
case 'Notification':
|
||||||
|
|
@ -860,6 +877,7 @@ export class Config {
|
||||||
(input['notification_type'] as NotificationType) ||
|
(input['notification_type'] as NotificationType) ||
|
||||||
'permission_prompt',
|
'permission_prompt',
|
||||||
(input['title'] as string) || undefined,
|
(input['title'] as string) || undefined,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'PermissionRequest':
|
case 'PermissionRequest':
|
||||||
|
|
@ -871,6 +889,7 @@ export class Config {
|
||||||
(input['permission_suggestions'] as
|
(input['permission_suggestions'] as
|
||||||
| PermissionSuggestion[]
|
| PermissionSuggestion[]
|
||||||
| undefined) || undefined,
|
| undefined) || undefined,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'SubagentStart':
|
case 'SubagentStart':
|
||||||
|
|
@ -879,6 +898,7 @@ export class Config {
|
||||||
(input['agent_type'] as string) || '',
|
(input['agent_type'] as string) || '',
|
||||||
(input['permission_mode'] as PermissionMode) ||
|
(input['permission_mode'] as PermissionMode) ||
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'SubagentStop':
|
case 'SubagentStop':
|
||||||
|
|
@ -890,6 +910,7 @@ export class Config {
|
||||||
(input['stop_hook_active'] as boolean) || false,
|
(input['stop_hook_active'] as boolean) || false,
|
||||||
(input['permission_mode'] as PermissionMode) ||
|
(input['permission_mode'] as PermissionMode) ||
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,17 @@ export class MessageBus extends EventEmitter {
|
||||||
request: Omit<TRequest, 'correlationId'>,
|
request: Omit<TRequest, 'correlationId'>,
|
||||||
responseType: TResponse['type'],
|
responseType: TResponse['type'],
|
||||||
timeoutMs: number = 60000,
|
timeoutMs: number = 60000,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<TResponse> {
|
): Promise<TResponse> {
|
||||||
const correlationId = randomUUID();
|
const correlationId = randomUUID();
|
||||||
|
|
||||||
return new Promise<TResponse>((resolve, reject) => {
|
return new Promise<TResponse>((resolve, reject) => {
|
||||||
|
// Check if already aborted
|
||||||
|
if (signal?.aborted) {
|
||||||
|
reject(new Error('Request aborted'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new Error(`Request timed out waiting for ${responseType}`));
|
reject(new Error(`Request timed out waiting for ${responseType}`));
|
||||||
|
|
@ -102,8 +109,20 @@ export class MessageBus extends EventEmitter {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
this.unsubscribe(responseType, responseHandler);
|
this.unsubscribe(responseType, responseHandler);
|
||||||
|
if (signal) {
|
||||||
|
signal.removeEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const abortHandler = () => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Request aborted'));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
|
|
||||||
const responseHandler = (response: TResponse) => {
|
const responseHandler = (response: TResponse) => {
|
||||||
// Check if this response matches our request
|
// Check if this response matches our request
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,8 @@ export interface HookExecutionRequest {
|
||||||
eventName: string;
|
eventName: string;
|
||||||
input: Record<string, unknown>;
|
input: Record<string, unknown>;
|
||||||
correlationId: string;
|
correlationId: string;
|
||||||
|
/** Optional AbortSignal to cancel hook execution */
|
||||||
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookExecutionResponse {
|
export interface HookExecutionResponse {
|
||||||
|
|
|
||||||
|
|
@ -535,7 +535,7 @@ export class GeminiClient {
|
||||||
return new Turn(this.getChat(), prompt_id);
|
return new Turn(this.getChat(), prompt_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const compressed = await this.tryCompressChat(prompt_id, false);
|
const compressed = await this.tryCompressChat(prompt_id, false, signal);
|
||||||
|
|
||||||
if (compressed.compressionStatus === CompressionStatus.COMPRESSED) {
|
if (compressed.compressionStatus === CompressionStatus.COMPRESSED) {
|
||||||
yield { type: GeminiEventType.ChatCompressed, value: compressed };
|
yield { type: GeminiEventType.ChatCompressed, value: compressed };
|
||||||
|
|
@ -677,7 +677,13 @@ export class GeminiClient {
|
||||||
}
|
}
|
||||||
// Fire Stop hook through MessageBus (only if hooks are enabled)
|
// Fire Stop hook through MessageBus (only if hooks are enabled)
|
||||||
// This must be done before any early returns to ensure hooks are always triggered
|
// This must be done before any early returns to ensure hooks are always triggered
|
||||||
if (hooksEnabled && messageBus && !turn.pendingToolCalls.length) {
|
if (
|
||||||
|
hooksEnabled &&
|
||||||
|
messageBus &&
|
||||||
|
!turn.pendingToolCalls.length &&
|
||||||
|
signal &&
|
||||||
|
!signal.aborted
|
||||||
|
) {
|
||||||
// Get response text from the chat history
|
// Get response text from the chat history
|
||||||
const history = this.getHistory();
|
const history = this.getHistory();
|
||||||
const lastModelMessage = history
|
const lastModelMessage = history
|
||||||
|
|
@ -700,9 +706,16 @@ export class GeminiClient {
|
||||||
stop_hook_active: true,
|
stop_hook_active: true,
|
||||||
last_assistant_message: responseText,
|
last_assistant_message: responseText,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if aborted after hook execution
|
||||||
|
if (signal.aborted) {
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
|
||||||
const hookOutput = response.output
|
const hookOutput = response.output
|
||||||
? createHookOutput('Stop', response.output)
|
? createHookOutput('Stop', response.output)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -714,6 +727,11 @@ export class GeminiClient {
|
||||||
stopOutput?.isBlockingDecision() ||
|
stopOutput?.isBlockingDecision() ||
|
||||||
stopOutput?.shouldStopExecution()
|
stopOutput?.shouldStopExecution()
|
||||||
) {
|
) {
|
||||||
|
// Check if aborted before continuing
|
||||||
|
if (signal.aborted) {
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
|
||||||
// Emit system message if provided (e.g., "🔄 Ralph iteration 5")
|
// Emit system message if provided (e.g., "🔄 Ralph iteration 5")
|
||||||
if (stopOutput.systemMessage) {
|
if (stopOutput.systemMessage) {
|
||||||
yield {
|
yield {
|
||||||
|
|
@ -844,6 +862,7 @@ export class GeminiClient {
|
||||||
async tryCompressChat(
|
async tryCompressChat(
|
||||||
prompt_id: string,
|
prompt_id: string,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<ChatCompressionInfo> {
|
): Promise<ChatCompressionInfo> {
|
||||||
const compressionService = new ChatCompressionService();
|
const compressionService = new ChatCompressionService();
|
||||||
|
|
||||||
|
|
@ -854,6 +873,7 @@ export class GeminiClient {
|
||||||
this.config.getModel(),
|
this.config.getModel(),
|
||||||
this.config,
|
this.config,
|
||||||
this.hasFailedCompressionAttempt,
|
this.hasFailedCompressionAttempt,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle compression result
|
// Handle compression result
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ export async function firePreToolUseHook(
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: string,
|
permissionMode: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<PreToolUseHookResult> {
|
): Promise<PreToolUseHookResult> {
|
||||||
if (!messageBus) {
|
if (!messageBus) {
|
||||||
return { shouldProceed: true };
|
return { shouldProceed: true };
|
||||||
|
|
@ -100,6 +101,7 @@ export async function firePreToolUseHook(
|
||||||
tool_input: toolInput,
|
tool_input: toolInput,
|
||||||
tool_use_id: toolUseId,
|
tool_use_id: toolUseId,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
@ -178,6 +180,7 @@ export async function firePostToolUseHook(
|
||||||
toolResponse: Record<string, unknown>,
|
toolResponse: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: string,
|
permissionMode: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<PostToolUseHookResult> {
|
): Promise<PostToolUseHookResult> {
|
||||||
if (!messageBus) {
|
if (!messageBus) {
|
||||||
return { shouldStop: false };
|
return { shouldStop: false };
|
||||||
|
|
@ -198,6 +201,7 @@ export async function firePostToolUseHook(
|
||||||
tool_response: toolResponse,
|
tool_response: toolResponse,
|
||||||
tool_use_id: toolUseId,
|
tool_use_id: toolUseId,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
@ -255,6 +259,7 @@ export async function firePostToolUseFailureHook(
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
isInterrupt?: boolean,
|
isInterrupt?: boolean,
|
||||||
permissionMode?: string,
|
permissionMode?: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<PostToolUseFailureHookResult> {
|
): Promise<PostToolUseFailureHookResult> {
|
||||||
if (!messageBus) {
|
if (!messageBus) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -276,6 +281,7 @@ export async function firePostToolUseFailureHook(
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
is_interrupt: isInterrupt,
|
is_interrupt: isInterrupt,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
@ -319,6 +325,7 @@ export async function fireNotificationHook(
|
||||||
message: string,
|
message: string,
|
||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
title?: string,
|
title?: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<NotificationHookResult> {
|
): Promise<NotificationHookResult> {
|
||||||
if (!messageBus) {
|
if (!messageBus) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -337,6 +344,7 @@ export async function fireNotificationHook(
|
||||||
notification_type: notificationType,
|
notification_type: notificationType,
|
||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
@ -390,6 +398,7 @@ export async function firePermissionRequestHook(
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
permissionMode: string,
|
permissionMode: string,
|
||||||
permissionSuggestions?: PermissionSuggestion[],
|
permissionSuggestions?: PermissionSuggestion[],
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<PermissionRequestHookResult> {
|
): Promise<PermissionRequestHookResult> {
|
||||||
if (!messageBus) {
|
if (!messageBus) {
|
||||||
return { hasDecision: false };
|
return { hasDecision: false };
|
||||||
|
|
@ -409,6 +418,7 @@ export async function firePermissionRequestHook(
|
||||||
permission_mode: permissionMode,
|
permission_mode: permissionMode,
|
||||||
permission_suggestions: permissionSuggestions,
|
permission_suggestions: permissionSuggestions,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,7 @@ describe('HookEventHandler', () => {
|
||||||
expect.any(Object), // input object
|
expect.any(Object), // input object
|
||||||
expect.any(Function), // onHookStart callback
|
expect.any(Function), // onHookStart callback
|
||||||
expect.any(Function), // onHookEnd callback
|
expect.any(Function), // onHookEnd callback
|
||||||
|
undefined, // signal
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,19 @@ export class HookEventHandler {
|
||||||
*/
|
*/
|
||||||
async fireUserPromptSubmitEvent(
|
async fireUserPromptSubmitEvent(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: UserPromptSubmitInput = {
|
const input: UserPromptSubmitInput = {
|
||||||
...this.createBaseInput(HookEventName.UserPromptSubmit),
|
...this.createBaseInput(HookEventName.UserPromptSubmit),
|
||||||
prompt,
|
prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.executeHooks(HookEventName.UserPromptSubmit, input);
|
return this.executeHooks(
|
||||||
|
HookEventName.UserPromptSubmit,
|
||||||
|
input,
|
||||||
|
undefined,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -80,6 +86,7 @@ export class HookEventHandler {
|
||||||
async fireStopEvent(
|
async fireStopEvent(
|
||||||
stopHookActive: boolean = false,
|
stopHookActive: boolean = false,
|
||||||
lastAssistantMessage: string = '',
|
lastAssistantMessage: string = '',
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: StopInput = {
|
const input: StopInput = {
|
||||||
...this.createBaseInput(HookEventName.Stop),
|
...this.createBaseInput(HookEventName.Stop),
|
||||||
|
|
@ -87,7 +94,7 @@ export class HookEventHandler {
|
||||||
last_assistant_message: lastAssistantMessage,
|
last_assistant_message: lastAssistantMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.executeHooks(HookEventName.Stop, input);
|
return this.executeHooks(HookEventName.Stop, input, undefined, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,6 +106,7 @@ export class HookEventHandler {
|
||||||
model: string,
|
model: string,
|
||||||
permissionMode?: PermissionMode,
|
permissionMode?: PermissionMode,
|
||||||
agentType?: AgentType,
|
agentType?: AgentType,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: SessionStartInput = {
|
const input: SessionStartInput = {
|
||||||
...this.createBaseInput(HookEventName.SessionStart),
|
...this.createBaseInput(HookEventName.SessionStart),
|
||||||
|
|
@ -109,9 +117,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass source as context for matcher filtering
|
// Pass source as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.SessionStart, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.SessionStart,
|
||||||
|
input,
|
||||||
|
{
|
||||||
trigger: source,
|
trigger: source,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,6 +133,7 @@ export class HookEventHandler {
|
||||||
*/
|
*/
|
||||||
async fireSessionEndEvent(
|
async fireSessionEndEvent(
|
||||||
reason: SessionEndReason,
|
reason: SessionEndReason,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: SessionEndInput = {
|
const input: SessionEndInput = {
|
||||||
...this.createBaseInput(HookEventName.SessionEnd),
|
...this.createBaseInput(HookEventName.SessionEnd),
|
||||||
|
|
@ -127,9 +141,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass reason as context for matcher filtering
|
// Pass reason as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.SessionEnd, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.SessionEnd,
|
||||||
|
input,
|
||||||
|
{
|
||||||
trigger: reason,
|
trigger: reason,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -141,6 +160,7 @@ export class HookEventHandler {
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: PreToolUseInput = {
|
const input: PreToolUseInput = {
|
||||||
...this.createBaseInput(HookEventName.PreToolUse),
|
...this.createBaseInput(HookEventName.PreToolUse),
|
||||||
|
|
@ -151,9 +171,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass tool name as context for matcher filtering
|
// Pass tool name as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.PreToolUse, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.PreToolUse,
|
||||||
|
input,
|
||||||
|
{
|
||||||
toolName,
|
toolName,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -166,6 +191,7 @@ export class HookEventHandler {
|
||||||
toolResponse: Record<string, unknown>,
|
toolResponse: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: PostToolUseInput = {
|
const input: PostToolUseInput = {
|
||||||
...this.createBaseInput(HookEventName.PostToolUse),
|
...this.createBaseInput(HookEventName.PostToolUse),
|
||||||
|
|
@ -177,9 +203,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass tool name as context for matcher filtering
|
// Pass tool name as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.PostToolUse, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.PostToolUse,
|
||||||
|
input,
|
||||||
|
{
|
||||||
toolName,
|
toolName,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -193,6 +224,7 @@ export class HookEventHandler {
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
isInterrupt?: boolean,
|
isInterrupt?: boolean,
|
||||||
permissionMode?: PermissionMode,
|
permissionMode?: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: PostToolUseFailureInput = {
|
const input: PostToolUseFailureInput = {
|
||||||
...this.createBaseInput(HookEventName.PostToolUseFailure),
|
...this.createBaseInput(HookEventName.PostToolUseFailure),
|
||||||
|
|
@ -205,9 +237,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass tool name as context for matcher filtering
|
// Pass tool name as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.PostToolUseFailure, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.PostToolUseFailure,
|
||||||
|
input,
|
||||||
|
{
|
||||||
toolName,
|
toolName,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -217,6 +254,7 @@ export class HookEventHandler {
|
||||||
async firePreCompactEvent(
|
async firePreCompactEvent(
|
||||||
trigger: PreCompactTrigger,
|
trigger: PreCompactTrigger,
|
||||||
customInstructions: string = '',
|
customInstructions: string = '',
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: PreCompactInput = {
|
const input: PreCompactInput = {
|
||||||
...this.createBaseInput(HookEventName.PreCompact),
|
...this.createBaseInput(HookEventName.PreCompact),
|
||||||
|
|
@ -225,9 +263,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass trigger as context for matcher filtering
|
// Pass trigger as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.PreCompact, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.PreCompact,
|
||||||
|
input,
|
||||||
|
{
|
||||||
trigger,
|
trigger,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -237,6 +280,7 @@ export class HookEventHandler {
|
||||||
message: string,
|
message: string,
|
||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
title?: string,
|
title?: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: NotificationInput = {
|
const input: NotificationInput = {
|
||||||
...this.createBaseInput(HookEventName.Notification),
|
...this.createBaseInput(HookEventName.Notification),
|
||||||
|
|
@ -246,9 +290,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass notification_type as context for matcher filtering
|
// Pass notification_type as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.Notification, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.Notification,
|
||||||
|
input,
|
||||||
|
{
|
||||||
notificationType,
|
notificationType,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -260,6 +309,7 @@ export class HookEventHandler {
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
permissionSuggestions?: PermissionSuggestion[],
|
permissionSuggestions?: PermissionSuggestion[],
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: PermissionRequestInput = {
|
const input: PermissionRequestInput = {
|
||||||
...this.createBaseInput(HookEventName.PermissionRequest),
|
...this.createBaseInput(HookEventName.PermissionRequest),
|
||||||
|
|
@ -270,9 +320,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass tool name as context for matcher filtering
|
// Pass tool name as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.PermissionRequest, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.PermissionRequest,
|
||||||
|
input,
|
||||||
|
{
|
||||||
toolName,
|
toolName,
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -283,6 +338,7 @@ export class HookEventHandler {
|
||||||
agentId: string,
|
agentId: string,
|
||||||
agentType: AgentType | string,
|
agentType: AgentType | string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: SubagentStartInput = {
|
const input: SubagentStartInput = {
|
||||||
...this.createBaseInput(HookEventName.SubagentStart),
|
...this.createBaseInput(HookEventName.SubagentStart),
|
||||||
|
|
@ -292,9 +348,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass agentType as context for matcher filtering
|
// Pass agentType as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.SubagentStart, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.SubagentStart,
|
||||||
|
input,
|
||||||
|
{
|
||||||
agentType: String(agentType),
|
agentType: String(agentType),
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -308,6 +369,7 @@ export class HookEventHandler {
|
||||||
lastAssistantMessage: string,
|
lastAssistantMessage: string,
|
||||||
stopHookActive: boolean,
|
stopHookActive: boolean,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
const input: SubagentStopInput = {
|
const input: SubagentStopInput = {
|
||||||
...this.createBaseInput(HookEventName.SubagentStop),
|
...this.createBaseInput(HookEventName.SubagentStop),
|
||||||
|
|
@ -320,9 +382,14 @@ export class HookEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass agentType as context for matcher filtering
|
// Pass agentType as context for matcher filtering
|
||||||
return this.executeHooks(HookEventName.SubagentStop, input, {
|
return this.executeHooks(
|
||||||
|
HookEventName.SubagentStop,
|
||||||
|
input,
|
||||||
|
{
|
||||||
agentType: String(agentType),
|
agentType: String(agentType),
|
||||||
});
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -333,6 +400,7 @@ export class HookEventHandler {
|
||||||
eventName: HookEventName,
|
eventName: HookEventName,
|
||||||
input: HookInput,
|
input: HookInput,
|
||||||
context?: HookEventContext,
|
context?: HookEventContext,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<AggregatedHookResult> {
|
): Promise<AggregatedHookResult> {
|
||||||
try {
|
try {
|
||||||
// Create execution plan
|
// Create execution plan
|
||||||
|
|
@ -363,6 +431,7 @@ export class HookEventHandler {
|
||||||
input,
|
input,
|
||||||
onHookStart,
|
onHookStart,
|
||||||
onHookEnd,
|
onHookEnd,
|
||||||
|
signal,
|
||||||
)
|
)
|
||||||
: await this.hookRunner.executeHooksParallel(
|
: await this.hookRunner.executeHooksParallel(
|
||||||
plan.hookConfigs,
|
plan.hookConfigs,
|
||||||
|
|
@ -370,6 +439,7 @@ export class HookEventHandler {
|
||||||
input,
|
input,
|
||||||
onHookStart,
|
onHookStart,
|
||||||
onHookEnd,
|
onHookEnd,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aggregate results
|
// Aggregate results
|
||||||
|
|
|
||||||
|
|
@ -46,20 +46,38 @@ const EXIT_CODE_NON_BLOCKING_ERROR = 1;
|
||||||
export class HookRunner {
|
export class HookRunner {
|
||||||
/**
|
/**
|
||||||
* Execute a single hook
|
* Execute a single hook
|
||||||
|
* @param hookConfig Hook configuration
|
||||||
|
* @param eventName Event name
|
||||||
|
* @param input Hook input
|
||||||
|
* @param signal Optional AbortSignal to cancel hook execution
|
||||||
*/
|
*/
|
||||||
async executeHook(
|
async executeHook(
|
||||||
hookConfig: HookConfig,
|
hookConfig: HookConfig,
|
||||||
eventName: HookEventName,
|
eventName: HookEventName,
|
||||||
input: HookInput,
|
input: HookInput,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<HookExecutionResult> {
|
): Promise<HookExecutionResult> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Check if already aborted before starting
|
||||||
|
if (signal?.aborted) {
|
||||||
|
const hookId = hookConfig.name || hookConfig.command || 'unknown';
|
||||||
|
return {
|
||||||
|
hookConfig,
|
||||||
|
eventName,
|
||||||
|
success: false,
|
||||||
|
error: new Error(`Hook execution cancelled (aborted): ${hookId}`),
|
||||||
|
duration: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.executeCommandHook(
|
return await this.executeCommandHook(
|
||||||
hookConfig,
|
hookConfig,
|
||||||
eventName,
|
eventName,
|
||||||
input,
|
input,
|
||||||
startTime,
|
startTime,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
@ -79,6 +97,7 @@ export class HookRunner {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute multiple hooks in parallel
|
* Execute multiple hooks in parallel
|
||||||
|
* @param signal Optional AbortSignal to cancel hook execution
|
||||||
*/
|
*/
|
||||||
async executeHooksParallel(
|
async executeHooksParallel(
|
||||||
hookConfigs: HookConfig[],
|
hookConfigs: HookConfig[],
|
||||||
|
|
@ -86,10 +105,11 @@ export class HookRunner {
|
||||||
input: HookInput,
|
input: HookInput,
|
||||||
onHookStart?: (config: HookConfig, index: number) => void,
|
onHookStart?: (config: HookConfig, index: number) => void,
|
||||||
onHookEnd?: (config: HookConfig, result: HookExecutionResult) => void,
|
onHookEnd?: (config: HookConfig, result: HookExecutionResult) => void,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<HookExecutionResult[]> {
|
): Promise<HookExecutionResult[]> {
|
||||||
const promises = hookConfigs.map(async (config, index) => {
|
const promises = hookConfigs.map(async (config, index) => {
|
||||||
onHookStart?.(config, index);
|
onHookStart?.(config, index);
|
||||||
const result = await this.executeHook(config, eventName, input);
|
const result = await this.executeHook(config, eventName, input, signal);
|
||||||
onHookEnd?.(config, result);
|
onHookEnd?.(config, result);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
@ -99,6 +119,7 @@ export class HookRunner {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute multiple hooks sequentially
|
* Execute multiple hooks sequentially
|
||||||
|
* @param signal Optional AbortSignal to cancel hook execution
|
||||||
*/
|
*/
|
||||||
async executeHooksSequential(
|
async executeHooksSequential(
|
||||||
hookConfigs: HookConfig[],
|
hookConfigs: HookConfig[],
|
||||||
|
|
@ -106,14 +127,24 @@ export class HookRunner {
|
||||||
input: HookInput,
|
input: HookInput,
|
||||||
onHookStart?: (config: HookConfig, index: number) => void,
|
onHookStart?: (config: HookConfig, index: number) => void,
|
||||||
onHookEnd?: (config: HookConfig, result: HookExecutionResult) => void,
|
onHookEnd?: (config: HookConfig, result: HookExecutionResult) => void,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<HookExecutionResult[]> {
|
): Promise<HookExecutionResult[]> {
|
||||||
const results: HookExecutionResult[] = [];
|
const results: HookExecutionResult[] = [];
|
||||||
let currentInput = input;
|
let currentInput = input;
|
||||||
|
|
||||||
for (let i = 0; i < hookConfigs.length; i++) {
|
for (let i = 0; i < hookConfigs.length; i++) {
|
||||||
|
// Check if aborted before each hook
|
||||||
|
if (signal?.aborted) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const config = hookConfigs[i];
|
const config = hookConfigs[i];
|
||||||
onHookStart?.(config, i);
|
onHookStart?.(config, i);
|
||||||
const result = await this.executeHook(config, eventName, currentInput);
|
const result = await this.executeHook(
|
||||||
|
config,
|
||||||
|
eventName,
|
||||||
|
currentInput,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
onHookEnd?.(config, result);
|
onHookEnd?.(config, result);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
|
|
||||||
|
|
@ -184,12 +215,18 @@ export class HookRunner {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a command hook
|
* Execute a command hook
|
||||||
|
* @param hookConfig Hook configuration
|
||||||
|
* @param eventName Event name
|
||||||
|
* @param input Hook input
|
||||||
|
* @param startTime Start time for duration calculation
|
||||||
|
* @param signal Optional AbortSignal to cancel hook execution
|
||||||
*/
|
*/
|
||||||
private async executeCommandHook(
|
private async executeCommandHook(
|
||||||
hookConfig: HookConfig,
|
hookConfig: HookConfig,
|
||||||
eventName: HookEventName,
|
eventName: HookEventName,
|
||||||
input: HookInput,
|
input: HookInput,
|
||||||
startTime: number,
|
startTime: number,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<HookExecutionResult> {
|
): Promise<HookExecutionResult> {
|
||||||
const timeout = hookConfig.timeout ?? DEFAULT_HOOK_TIMEOUT;
|
const timeout = hookConfig.timeout ?? DEFAULT_HOOK_TIMEOUT;
|
||||||
|
|
||||||
|
|
@ -212,6 +249,7 @@ export class HookRunner {
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
|
let aborted = false;
|
||||||
|
|
||||||
const shellConfig = getShellConfiguration();
|
const shellConfig = getShellConfiguration();
|
||||||
const command = this.expandCommand(
|
const command = this.expandCommand(
|
||||||
|
|
@ -239,19 +277,36 @@ export class HookRunner {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set up timeout
|
// Helper to kill child process
|
||||||
const timeoutHandle = setTimeout(() => {
|
const killChild = () => {
|
||||||
timedOut = true;
|
if (!child.killed) {
|
||||||
child.kill('SIGTERM');
|
child.kill('SIGTERM');
|
||||||
|
// Force kill after 2 seconds
|
||||||
// Force kill after 5 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!child.killed) {
|
if (!child.killed) {
|
||||||
child.kill('SIGKILL');
|
child.kill('SIGKILL');
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up timeout
|
||||||
|
const timeoutHandle = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
killChild();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
// Set up abort handler
|
||||||
|
const abortHandler = () => {
|
||||||
|
aborted = true;
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
killChild();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// Send input to stdin
|
// Send input to stdin
|
||||||
if (child.stdin) {
|
if (child.stdin) {
|
||||||
child.stdin.on('error', (err: NodeJS.ErrnoException) => {
|
child.stdin.on('error', (err: NodeJS.ErrnoException) => {
|
||||||
|
|
@ -303,8 +358,25 @@ export class HookRunner {
|
||||||
// Handle process exit
|
// Handle process exit
|
||||||
child.on('close', (exitCode) => {
|
child.on('close', (exitCode) => {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
|
// Clean up abort listener
|
||||||
|
if (signal) {
|
||||||
|
signal.removeEventListener('abort', abortHandler);
|
||||||
|
}
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (aborted) {
|
||||||
|
resolve({
|
||||||
|
hookConfig,
|
||||||
|
eventName,
|
||||||
|
success: false,
|
||||||
|
error: new Error('Hook execution cancelled (aborted)'),
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
resolve({
|
resolve({
|
||||||
hookConfig,
|
hookConfig,
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,7 @@ describe('HookSystem', () => {
|
||||||
expect(mockHookEventHandler.fireStopEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.fireStopEvent).toHaveBeenCalledWith(
|
||||||
true,
|
true,
|
||||||
'last message',
|
'last message',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -228,6 +229,7 @@ describe('HookSystem', () => {
|
||||||
expect(mockHookEventHandler.fireStopEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.fireStopEvent).toHaveBeenCalledWith(
|
||||||
false,
|
false,
|
||||||
'',
|
'',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -269,7 +271,7 @@ describe('HookSystem', () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mockHookEventHandler.fireUserPromptSubmitEvent,
|
mockHookEventHandler.fireUserPromptSubmitEvent,
|
||||||
).toHaveBeenCalledWith('test prompt');
|
).toHaveBeenCalledWith('test prompt', undefined);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -291,7 +293,7 @@ describe('HookSystem', () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mockHookEventHandler.fireUserPromptSubmitEvent,
|
mockHookEventHandler.fireUserPromptSubmitEvent,
|
||||||
).toHaveBeenCalledWith('my custom prompt');
|
).toHaveBeenCalledWith('my custom prompt', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined when no final output', async () => {
|
it('should return undefined when no final output', async () => {
|
||||||
|
|
@ -382,6 +384,7 @@ describe('HookSystem', () => {
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -412,6 +415,7 @@ describe('HookSystem', () => {
|
||||||
'claude-3',
|
'claude-3',
|
||||||
PermissionMode.AutoEdit,
|
PermissionMode.AutoEdit,
|
||||||
AgentType.Custom,
|
AgentType.Custom,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -458,6 +462,7 @@ describe('HookSystem', () => {
|
||||||
|
|
||||||
expect(mockHookEventHandler.fireSessionEndEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.fireSessionEndEvent).toHaveBeenCalledWith(
|
||||||
SessionEndReason.Other,
|
SessionEndReason.Other,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -480,6 +485,7 @@ describe('HookSystem', () => {
|
||||||
|
|
||||||
expect(mockHookEventHandler.fireSessionEndEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.fireSessionEndEvent).toHaveBeenCalledWith(
|
||||||
SessionEndReason.Other,
|
SessionEndReason.Other,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -531,6 +537,7 @@ describe('HookSystem', () => {
|
||||||
{ command: 'ls' },
|
{ command: 'ls' },
|
||||||
'toolu_test123',
|
'toolu_test123',
|
||||||
PermissionMode.AutoEdit,
|
PermissionMode.AutoEdit,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -561,6 +568,7 @@ describe('HookSystem', () => {
|
||||||
{ path: '/test.txt', content: 'test' },
|
{ path: '/test.txt', content: 'test' },
|
||||||
'toolu_test456',
|
'toolu_test456',
|
||||||
PermissionMode.Yolo,
|
PermissionMode.Yolo,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -674,6 +682,7 @@ describe('HookSystem', () => {
|
||||||
{ output: 'file1.txt\nfile2.txt' },
|
{ output: 'file1.txt\nfile2.txt' },
|
||||||
'toolu_test123',
|
'toolu_test123',
|
||||||
PermissionMode.AutoEdit,
|
PermissionMode.AutoEdit,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -706,6 +715,7 @@ describe('HookSystem', () => {
|
||||||
{ content: 'file content' },
|
{ content: 'file content' },
|
||||||
'toolu_test456',
|
'toolu_test456',
|
||||||
PermissionMode.Plan,
|
PermissionMode.Plan,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -794,6 +804,7 @@ describe('HookSystem', () => {
|
||||||
'Command not found',
|
'Command not found',
|
||||||
false,
|
false,
|
||||||
PermissionMode.AutoEdit,
|
PermissionMode.AutoEdit,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -830,6 +841,7 @@ describe('HookSystem', () => {
|
||||||
'Permission denied',
|
'Permission denied',
|
||||||
true,
|
true,
|
||||||
PermissionMode.Yolo,
|
PermissionMode.Yolo,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -861,6 +873,7 @@ describe('HookSystem', () => {
|
||||||
'Error occurred',
|
'Error occurred',
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -941,6 +954,7 @@ describe('HookSystem', () => {
|
||||||
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
||||||
PreCompactTrigger.Auto,
|
PreCompactTrigger.Auto,
|
||||||
'',
|
'',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -964,6 +978,7 @@ describe('HookSystem', () => {
|
||||||
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
||||||
PreCompactTrigger.Manual,
|
PreCompactTrigger.Manual,
|
||||||
'',
|
'',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -989,6 +1004,7 @@ describe('HookSystem', () => {
|
||||||
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith(
|
||||||
PreCompactTrigger.Auto,
|
PreCompactTrigger.Auto,
|
||||||
'Custom compression instructions',
|
'Custom compression instructions',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1065,6 +1081,7 @@ describe('HookSystem', () => {
|
||||||
'Test notification message',
|
'Test notification message',
|
||||||
NotificationType.PermissionPrompt,
|
NotificationType.PermissionPrompt,
|
||||||
'Permission needed',
|
'Permission needed',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -1093,6 +1110,7 @@ describe('HookSystem', () => {
|
||||||
'Qwen Code is waiting for your input',
|
'Qwen Code is waiting for your input',
|
||||||
NotificationType.IdlePrompt,
|
NotificationType.IdlePrompt,
|
||||||
'Waiting for input',
|
'Waiting for input',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1119,6 +1137,7 @@ describe('HookSystem', () => {
|
||||||
'Authentication successful',
|
'Authentication successful',
|
||||||
NotificationType.AuthSuccess,
|
NotificationType.AuthSuccess,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1194,6 +1213,7 @@ describe('HookSystem', () => {
|
||||||
'Dialog shown to user',
|
'Dialog shown to user',
|
||||||
NotificationType.ElicitationDialog,
|
NotificationType.ElicitationDialog,
|
||||||
'Dialog',
|
'Dialog',
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1226,6 +1246,7 @@ describe('HookSystem', () => {
|
||||||
{ command: 'ls -la' },
|
{ command: 'ls -la' },
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
// Type assertion needed because getPermissionDecision is specific to PermissionRequestHookOutput
|
// Type assertion needed because getPermissionDecision is specific to PermissionRequestHookOutput
|
||||||
|
|
@ -1259,6 +1280,7 @@ describe('HookSystem', () => {
|
||||||
{ command: 'npm test' },
|
{ command: 'npm test' },
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
suggestions,
|
suggestions,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1354,6 +1376,7 @@ describe('HookSystem', () => {
|
||||||
'agent-123',
|
'agent-123',
|
||||||
'code-reviewer',
|
'code-reviewer',
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -1382,6 +1405,7 @@ describe('HookSystem', () => {
|
||||||
'agent-456',
|
'agent-456',
|
||||||
AgentType.Bash,
|
AgentType.Bash,
|
||||||
PermissionMode.Yolo,
|
PermissionMode.Yolo,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1468,6 +1492,7 @@ describe('HookSystem', () => {
|
||||||
'Final output from subagent',
|
'Final output from subagent',
|
||||||
false,
|
false,
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -1502,6 +1527,7 @@ describe('HookSystem', () => {
|
||||||
'last message from agent',
|
'last message from agent',
|
||||||
true,
|
true,
|
||||||
PermissionMode.Plan,
|
PermissionMode.Plan,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,12 @@ export class HookSystem {
|
||||||
|
|
||||||
async fireUserPromptSubmitEvent(
|
async fireUserPromptSubmitEvent(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result =
|
const result = await this.hookEventHandler.fireUserPromptSubmitEvent(
|
||||||
await this.hookEventHandler.fireUserPromptSubmitEvent(prompt);
|
prompt,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('UserPromptSubmit', result.finalOutput)
|
? createHookOutput('UserPromptSubmit', result.finalOutput)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -100,10 +103,12 @@ export class HookSystem {
|
||||||
async fireStopEvent(
|
async fireStopEvent(
|
||||||
stopHookActive: boolean = false,
|
stopHookActive: boolean = false,
|
||||||
lastAssistantMessage: string = '',
|
lastAssistantMessage: string = '',
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireStopEvent(
|
const result = await this.hookEventHandler.fireStopEvent(
|
||||||
stopHookActive,
|
stopHookActive,
|
||||||
lastAssistantMessage,
|
lastAssistantMessage,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('Stop', result.finalOutput)
|
? createHookOutput('Stop', result.finalOutput)
|
||||||
|
|
@ -115,12 +120,14 @@ export class HookSystem {
|
||||||
model: string,
|
model: string,
|
||||||
permissionMode?: PermissionMode,
|
permissionMode?: PermissionMode,
|
||||||
agentType?: AgentType,
|
agentType?: AgentType,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireSessionStartEvent(
|
const result = await this.hookEventHandler.fireSessionStartEvent(
|
||||||
source,
|
source,
|
||||||
model,
|
model,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
agentType,
|
agentType,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('SessionStart', result.finalOutput)
|
? createHookOutput('SessionStart', result.finalOutput)
|
||||||
|
|
@ -129,8 +136,12 @@ export class HookSystem {
|
||||||
|
|
||||||
async fireSessionEndEvent(
|
async fireSessionEndEvent(
|
||||||
reason: SessionEndReason,
|
reason: SessionEndReason,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireSessionEndEvent(reason);
|
const result = await this.hookEventHandler.fireSessionEndEvent(
|
||||||
|
reason,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('SessionEnd', result.finalOutput)
|
? createHookOutput('SessionEnd', result.finalOutput)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -144,12 +155,14 @@ export class HookSystem {
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.firePreToolUseEvent(
|
const result = await this.hookEventHandler.firePreToolUseEvent(
|
||||||
toolName,
|
toolName,
|
||||||
toolInput,
|
toolInput,
|
||||||
toolUseId,
|
toolUseId,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('PreToolUse', result.finalOutput)
|
? createHookOutput('PreToolUse', result.finalOutput)
|
||||||
|
|
@ -165,6 +178,7 @@ export class HookSystem {
|
||||||
toolResponse: Record<string, unknown>,
|
toolResponse: Record<string, unknown>,
|
||||||
toolUseId: string,
|
toolUseId: string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.firePostToolUseEvent(
|
const result = await this.hookEventHandler.firePostToolUseEvent(
|
||||||
toolName,
|
toolName,
|
||||||
|
|
@ -172,6 +186,7 @@ export class HookSystem {
|
||||||
toolResponse,
|
toolResponse,
|
||||||
toolUseId,
|
toolUseId,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('PostToolUse', result.finalOutput)
|
? createHookOutput('PostToolUse', result.finalOutput)
|
||||||
|
|
@ -188,6 +203,7 @@ export class HookSystem {
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
isInterrupt?: boolean,
|
isInterrupt?: boolean,
|
||||||
permissionMode?: PermissionMode,
|
permissionMode?: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.firePostToolUseFailureEvent(
|
const result = await this.hookEventHandler.firePostToolUseFailureEvent(
|
||||||
toolUseId,
|
toolUseId,
|
||||||
|
|
@ -196,6 +212,7 @@ export class HookSystem {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isInterrupt,
|
isInterrupt,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('PostToolUseFailure', result.finalOutput)
|
? createHookOutput('PostToolUseFailure', result.finalOutput)
|
||||||
|
|
@ -208,10 +225,12 @@ export class HookSystem {
|
||||||
async firePreCompactEvent(
|
async firePreCompactEvent(
|
||||||
trigger: PreCompactTrigger,
|
trigger: PreCompactTrigger,
|
||||||
customInstructions: string = '',
|
customInstructions: string = '',
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.firePreCompactEvent(
|
const result = await this.hookEventHandler.firePreCompactEvent(
|
||||||
trigger,
|
trigger,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('PreCompact', result.finalOutput)
|
? createHookOutput('PreCompact', result.finalOutput)
|
||||||
|
|
@ -225,11 +244,13 @@ export class HookSystem {
|
||||||
message: string,
|
message: string,
|
||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
title?: string,
|
title?: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireNotificationEvent(
|
const result = await this.hookEventHandler.fireNotificationEvent(
|
||||||
message,
|
message,
|
||||||
notificationType,
|
notificationType,
|
||||||
title,
|
title,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('Notification', result.finalOutput)
|
? createHookOutput('Notification', result.finalOutput)
|
||||||
|
|
@ -243,11 +264,13 @@ export class HookSystem {
|
||||||
agentId: string,
|
agentId: string,
|
||||||
agentType: AgentType | string,
|
agentType: AgentType | string,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireSubagentStartEvent(
|
const result = await this.hookEventHandler.fireSubagentStartEvent(
|
||||||
agentId,
|
agentId,
|
||||||
agentType,
|
agentType,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('SubagentStart', result.finalOutput)
|
? createHookOutput('SubagentStart', result.finalOutput)
|
||||||
|
|
@ -264,6 +287,7 @@ export class HookSystem {
|
||||||
lastAssistantMessage: string,
|
lastAssistantMessage: string,
|
||||||
stopHookActive: boolean,
|
stopHookActive: boolean,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.fireSubagentStopEvent(
|
const result = await this.hookEventHandler.fireSubagentStopEvent(
|
||||||
agentId,
|
agentId,
|
||||||
|
|
@ -272,6 +296,7 @@ export class HookSystem {
|
||||||
lastAssistantMessage,
|
lastAssistantMessage,
|
||||||
stopHookActive,
|
stopHookActive,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('SubagentStop', result.finalOutput)
|
? createHookOutput('SubagentStop', result.finalOutput)
|
||||||
|
|
@ -286,12 +311,14 @@ export class HookSystem {
|
||||||
toolInput: Record<string, unknown>,
|
toolInput: Record<string, unknown>,
|
||||||
permissionMode: PermissionMode,
|
permissionMode: PermissionMode,
|
||||||
permissionSuggestions?: PermissionSuggestion[],
|
permissionSuggestions?: PermissionSuggestion[],
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<DefaultHookOutput | undefined> {
|
): Promise<DefaultHookOutput | undefined> {
|
||||||
const result = await this.hookEventHandler.firePermissionRequestEvent(
|
const result = await this.hookEventHandler.firePermissionRequestEvent(
|
||||||
toolName,
|
toolName,
|
||||||
toolInput,
|
toolInput,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
permissionSuggestions,
|
permissionSuggestions,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
return result.finalOutput
|
return result.finalOutput
|
||||||
? createHookOutput('PermissionRequest', result.finalOutput)
|
? createHookOutput('PermissionRequest', result.finalOutput)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { getCompressionPrompt } from '../core/prompts.js';
|
||||||
import { getResponseText } from '../utils/partUtils.js';
|
import { getResponseText } from '../utils/partUtils.js';
|
||||||
import { logChatCompression } from '../telemetry/loggers.js';
|
import { logChatCompression } from '../telemetry/loggers.js';
|
||||||
import { makeChatCompressionEvent } from '../telemetry/types.js';
|
import { makeChatCompressionEvent } from '../telemetry/types.js';
|
||||||
|
import type { PermissionMode } from '../hooks/types.js';
|
||||||
import { SessionStartSource, PreCompactTrigger } from '../hooks/types.js';
|
import { SessionStartSource, PreCompactTrigger } from '../hooks/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,6 +85,7 @@ export class ChatCompressionService {
|
||||||
model: string,
|
model: string,
|
||||||
config: Config,
|
config: Config,
|
||||||
hasFailedCompressionAttempt: boolean,
|
hasFailedCompressionAttempt: boolean,
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<{ newHistory: Content[] | null; info: ChatCompressionInfo }> {
|
): Promise<{ newHistory: Content[] | null; info: ChatCompressionInfo }> {
|
||||||
const curatedHistory = chat.getHistory(true);
|
const curatedHistory = chat.getHistory(true);
|
||||||
const threshold =
|
const threshold =
|
||||||
|
|
@ -130,7 +132,7 @@ export class ChatCompressionService {
|
||||||
if (hookSystem) {
|
if (hookSystem) {
|
||||||
const trigger = force ? PreCompactTrigger.Manual : PreCompactTrigger.Auto;
|
const trigger = force ? PreCompactTrigger.Manual : PreCompactTrigger.Auto;
|
||||||
try {
|
try {
|
||||||
await hookSystem.firePreCompactEvent(trigger, '');
|
await hookSystem.firePreCompactEvent(trigger, '', signal);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
config.getDebugLogger().warn(`PreCompact hook failed: ${err}`);
|
config.getDebugLogger().warn(`PreCompact hook failed: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
@ -276,9 +278,18 @@ export class ChatCompressionService {
|
||||||
|
|
||||||
// Fire SessionStart event after successful compression
|
// Fire SessionStart event after successful compression
|
||||||
try {
|
try {
|
||||||
|
const permissionMode = String(
|
||||||
|
config.getApprovalMode(),
|
||||||
|
) as PermissionMode;
|
||||||
await config
|
await config
|
||||||
.getHookSystem()
|
.getHookSystem()
|
||||||
?.fireSessionStartEvent(SessionStartSource.Compact, model ?? '');
|
?.fireSessionStartEvent(
|
||||||
|
SessionStartSource.Compact,
|
||||||
|
model ?? '',
|
||||||
|
permissionMode,
|
||||||
|
undefined,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,7 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
|
||||||
agentId,
|
agentId,
|
||||||
agentType,
|
agentType,
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inject additional context from hook output into subagent context
|
// Inject additional context from hook output into subagent context
|
||||||
|
|
@ -572,6 +573,7 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
|
||||||
subagent.getFinalText(),
|
subagent.getFinalText(),
|
||||||
stopHookActive,
|
stopHookActive,
|
||||||
PermissionMode.Default,
|
PermissionMode.Default,
|
||||||
|
signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
const typedStopOutput = stopHookOutput as
|
const typedStopOutput = stopHookOutput as
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue