| |
| |
| |
| |
| |
| |
|
|
| import type { PluginRegistry } from "./registry.js"; |
| import type { |
| PluginHookAfterCompactionEvent, |
| PluginHookAfterToolCallEvent, |
| PluginHookAgentContext, |
| PluginHookAgentEndEvent, |
| PluginHookBeforeAgentStartEvent, |
| PluginHookBeforeAgentStartResult, |
| PluginHookBeforeCompactionEvent, |
| PluginHookBeforeToolCallEvent, |
| PluginHookBeforeToolCallResult, |
| PluginHookGatewayContext, |
| PluginHookGatewayStartEvent, |
| PluginHookGatewayStopEvent, |
| PluginHookMessageContext, |
| PluginHookMessageReceivedEvent, |
| PluginHookMessageSendingEvent, |
| PluginHookMessageSendingResult, |
| PluginHookMessageSentEvent, |
| PluginHookName, |
| PluginHookRegistration, |
| PluginHookSessionContext, |
| PluginHookSessionEndEvent, |
| PluginHookSessionStartEvent, |
| PluginHookToolContext, |
| PluginHookToolResultPersistContext, |
| PluginHookToolResultPersistEvent, |
| PluginHookToolResultPersistResult, |
| } from "./types.js"; |
|
|
| |
| export type { |
| PluginHookAgentContext, |
| PluginHookBeforeAgentStartEvent, |
| PluginHookBeforeAgentStartResult, |
| PluginHookAgentEndEvent, |
| PluginHookBeforeCompactionEvent, |
| PluginHookAfterCompactionEvent, |
| PluginHookMessageContext, |
| PluginHookMessageReceivedEvent, |
| PluginHookMessageSendingEvent, |
| PluginHookMessageSendingResult, |
| PluginHookMessageSentEvent, |
| PluginHookToolContext, |
| PluginHookBeforeToolCallEvent, |
| PluginHookBeforeToolCallResult, |
| PluginHookAfterToolCallEvent, |
| PluginHookToolResultPersistContext, |
| PluginHookToolResultPersistEvent, |
| PluginHookToolResultPersistResult, |
| PluginHookSessionContext, |
| PluginHookSessionStartEvent, |
| PluginHookSessionEndEvent, |
| PluginHookGatewayContext, |
| PluginHookGatewayStartEvent, |
| PluginHookGatewayStopEvent, |
| }; |
|
|
| export type HookRunnerLogger = { |
| debug?: (message: string) => void; |
| warn: (message: string) => void; |
| error: (message: string) => void; |
| }; |
|
|
| export type HookRunnerOptions = { |
| logger?: HookRunnerLogger; |
| |
| catchErrors?: boolean; |
| }; |
|
|
| |
| |
| |
| function getHooksForName<K extends PluginHookName>( |
| registry: PluginRegistry, |
| hookName: K, |
| ): PluginHookRegistration<K>[] { |
| return (registry.typedHooks as PluginHookRegistration<K>[]) |
| .filter((h) => h.hookName === hookName) |
| .toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); |
| } |
|
|
| |
| |
| |
| export function createHookRunner(registry: PluginRegistry, options: HookRunnerOptions = {}) { |
| const logger = options.logger; |
| const catchErrors = options.catchErrors ?? true; |
|
|
| |
| |
| |
| |
| async function runVoidHook<K extends PluginHookName>( |
| hookName: K, |
| event: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[0], |
| ctx: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[1], |
| ): Promise<void> { |
| const hooks = getHooksForName(registry, hookName); |
| if (hooks.length === 0) { |
| return; |
| } |
|
|
| logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`); |
|
|
| const promises = hooks.map(async (hook) => { |
| try { |
| await (hook.handler as (event: unknown, ctx: unknown) => Promise<void>)(event, ctx); |
| } catch (err) { |
| const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`; |
| if (catchErrors) { |
| logger?.error(msg); |
| } else { |
| throw new Error(msg, { cause: err }); |
| } |
| } |
| }); |
|
|
| await Promise.all(promises); |
| } |
|
|
| |
| |
| |
| |
| async function runModifyingHook<K extends PluginHookName, TResult>( |
| hookName: K, |
| event: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[0], |
| ctx: Parameters<NonNullable<PluginHookRegistration<K>["handler"]>>[1], |
| mergeResults?: (accumulated: TResult | undefined, next: TResult) => TResult, |
| ): Promise<TResult | undefined> { |
| const hooks = getHooksForName(registry, hookName); |
| if (hooks.length === 0) { |
| return undefined; |
| } |
|
|
| logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, sequential)`); |
|
|
| let result: TResult | undefined; |
|
|
| for (const hook of hooks) { |
| try { |
| const handlerResult = await ( |
| hook.handler as (event: unknown, ctx: unknown) => Promise<TResult> |
| )(event, ctx); |
|
|
| if (handlerResult !== undefined && handlerResult !== null) { |
| if (mergeResults && result !== undefined) { |
| result = mergeResults(result, handlerResult); |
| } else { |
| result = handlerResult; |
| } |
| } |
| } catch (err) { |
| const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`; |
| if (catchErrors) { |
| logger?.error(msg); |
| } else { |
| throw new Error(msg, { cause: err }); |
| } |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| async function runBeforeAgentStart( |
| event: PluginHookBeforeAgentStartEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<PluginHookBeforeAgentStartResult | undefined> { |
| return runModifyingHook<"before_agent_start", PluginHookBeforeAgentStartResult>( |
| "before_agent_start", |
| event, |
| ctx, |
| (acc, next) => ({ |
| systemPrompt: next.systemPrompt ?? acc?.systemPrompt, |
| prependContext: |
| acc?.prependContext && next.prependContext |
| ? `${acc.prependContext}\n\n${next.prependContext}` |
| : (next.prependContext ?? acc?.prependContext), |
| }), |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| async function runAgentEnd( |
| event: PluginHookAgentEndEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<void> { |
| return runVoidHook("agent_end", event, ctx); |
| } |
|
|
| |
| |
| |
| async function runBeforeCompaction( |
| event: PluginHookBeforeCompactionEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<void> { |
| return runVoidHook("before_compaction", event, ctx); |
| } |
|
|
| |
| |
| |
| async function runAfterCompaction( |
| event: PluginHookAfterCompactionEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<void> { |
| return runVoidHook("after_compaction", event, ctx); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| async function runMessageReceived( |
| event: PluginHookMessageReceivedEvent, |
| ctx: PluginHookMessageContext, |
| ): Promise<void> { |
| return runVoidHook("message_received", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| |
| async function runMessageSending( |
| event: PluginHookMessageSendingEvent, |
| ctx: PluginHookMessageContext, |
| ): Promise<PluginHookMessageSendingResult | undefined> { |
| return runModifyingHook<"message_sending", PluginHookMessageSendingResult>( |
| "message_sending", |
| event, |
| ctx, |
| (acc, next) => ({ |
| content: next.content ?? acc?.content, |
| cancel: next.cancel ?? acc?.cancel, |
| }), |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runMessageSent( |
| event: PluginHookMessageSentEvent, |
| ctx: PluginHookMessageContext, |
| ): Promise<void> { |
| return runVoidHook("message_sent", event, ctx); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| async function runBeforeToolCall( |
| event: PluginHookBeforeToolCallEvent, |
| ctx: PluginHookToolContext, |
| ): Promise<PluginHookBeforeToolCallResult | undefined> { |
| return runModifyingHook<"before_tool_call", PluginHookBeforeToolCallResult>( |
| "before_tool_call", |
| event, |
| ctx, |
| (acc, next) => ({ |
| params: next.params ?? acc?.params, |
| block: next.block ?? acc?.block, |
| blockReason: next.blockReason ?? acc?.blockReason, |
| }), |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runAfterToolCall( |
| event: PluginHookAfterToolCallEvent, |
| ctx: PluginHookToolContext, |
| ): Promise<void> { |
| return runVoidHook("after_tool_call", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function runToolResultPersist( |
| event: PluginHookToolResultPersistEvent, |
| ctx: PluginHookToolResultPersistContext, |
| ): PluginHookToolResultPersistResult | undefined { |
| const hooks = getHooksForName(registry, "tool_result_persist"); |
| if (hooks.length === 0) { |
| return undefined; |
| } |
|
|
| let current = event.message; |
|
|
| for (const hook of hooks) { |
| try { |
| const out = (hook.handler as any)({ ...event, message: current }, ctx) as |
| | PluginHookToolResultPersistResult |
| | void |
| | Promise<unknown>; |
|
|
| |
| if (out && typeof (out as any).then === "function") { |
| const msg = |
| `[hooks] tool_result_persist handler from ${hook.pluginId} returned a Promise; ` + |
| `this hook is synchronous and the result was ignored.`; |
| if (catchErrors) { |
| logger?.warn?.(msg); |
| continue; |
| } |
| throw new Error(msg); |
| } |
|
|
| const next = (out as PluginHookToolResultPersistResult | undefined)?.message; |
| if (next) { |
| current = next; |
| } |
| } catch (err) { |
| const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} failed: ${String(err)}`; |
| if (catchErrors) { |
| logger?.error(msg); |
| } else { |
| throw new Error(msg, { cause: err }); |
| } |
| } |
| } |
|
|
| return { message: current }; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| async function runSessionStart( |
| event: PluginHookSessionStartEvent, |
| ctx: PluginHookSessionContext, |
| ): Promise<void> { |
| return runVoidHook("session_start", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| async function runSessionEnd( |
| event: PluginHookSessionEndEvent, |
| ctx: PluginHookSessionContext, |
| ): Promise<void> { |
| return runVoidHook("session_end", event, ctx); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| async function runGatewayStart( |
| event: PluginHookGatewayStartEvent, |
| ctx: PluginHookGatewayContext, |
| ): Promise<void> { |
| return runVoidHook("gateway_start", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| async function runGatewayStop( |
| event: PluginHookGatewayStopEvent, |
| ctx: PluginHookGatewayContext, |
| ): Promise<void> { |
| return runVoidHook("gateway_stop", event, ctx); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| function hasHooks(hookName: PluginHookName): boolean { |
| return registry.typedHooks.some((h) => h.hookName === hookName); |
| } |
|
|
| |
| |
| |
| function getHookCount(hookName: PluginHookName): number { |
| return registry.typedHooks.filter((h) => h.hookName === hookName).length; |
| } |
|
|
| return { |
| |
| runBeforeAgentStart, |
| runAgentEnd, |
| runBeforeCompaction, |
| runAfterCompaction, |
| |
| runMessageReceived, |
| runMessageSending, |
| runMessageSent, |
| |
| runBeforeToolCall, |
| runAfterToolCall, |
| runToolResultPersist, |
| |
| runSessionStart, |
| runSessionEnd, |
| |
| runGatewayStart, |
| runGatewayStop, |
| |
| hasHooks, |
| getHookCount, |
| }; |
| } |
|
|
| export type HookRunner = ReturnType<typeof createHookRunner>; |
|
|