| |
| |
| |
| |
| |
| |
|
|
| import { concatOptionalTextSegments } from "../shared/text/join-segments.js"; |
| import type { PluginRegistry } from "./registry.js"; |
| import type { |
| PluginHookAfterCompactionEvent, |
| PluginHookAfterToolCallEvent, |
| PluginHookAgentContext, |
| PluginHookAgentEndEvent, |
| PluginHookBeforeAgentStartEvent, |
| PluginHookBeforeAgentStartResult, |
| PluginHookBeforeModelResolveEvent, |
| PluginHookBeforeModelResolveResult, |
| PluginHookBeforePromptBuildEvent, |
| PluginHookBeforePromptBuildResult, |
| PluginHookBeforeCompactionEvent, |
| PluginHookLlmInputEvent, |
| PluginHookLlmOutputEvent, |
| PluginHookBeforeResetEvent, |
| PluginHookBeforeToolCallEvent, |
| PluginHookBeforeToolCallResult, |
| PluginHookGatewayContext, |
| PluginHookGatewayStartEvent, |
| PluginHookGatewayStopEvent, |
| PluginHookMessageContext, |
| PluginHookMessageReceivedEvent, |
| PluginHookMessageSendingEvent, |
| PluginHookMessageSendingResult, |
| PluginHookMessageSentEvent, |
| PluginHookName, |
| PluginHookRegistration, |
| PluginHookSessionContext, |
| PluginHookSessionEndEvent, |
| PluginHookSessionStartEvent, |
| PluginHookSubagentContext, |
| PluginHookSubagentDeliveryTargetEvent, |
| PluginHookSubagentDeliveryTargetResult, |
| PluginHookSubagentSpawningEvent, |
| PluginHookSubagentSpawningResult, |
| PluginHookSubagentEndedEvent, |
| PluginHookSubagentSpawnedEvent, |
| PluginHookToolContext, |
| PluginHookToolResultPersistContext, |
| PluginHookToolResultPersistEvent, |
| PluginHookToolResultPersistResult, |
| PluginHookBeforeMessageWriteEvent, |
| PluginHookBeforeMessageWriteResult, |
| } from "./types.js"; |
|
|
| |
| export type { |
| PluginHookAgentContext, |
| PluginHookBeforeAgentStartEvent, |
| PluginHookBeforeAgentStartResult, |
| PluginHookBeforeModelResolveEvent, |
| PluginHookBeforeModelResolveResult, |
| PluginHookBeforePromptBuildEvent, |
| PluginHookBeforePromptBuildResult, |
| PluginHookLlmInputEvent, |
| PluginHookLlmOutputEvent, |
| PluginHookAgentEndEvent, |
| PluginHookBeforeCompactionEvent, |
| PluginHookBeforeResetEvent, |
| PluginHookAfterCompactionEvent, |
| PluginHookMessageContext, |
| PluginHookMessageReceivedEvent, |
| PluginHookMessageSendingEvent, |
| PluginHookMessageSendingResult, |
| PluginHookMessageSentEvent, |
| PluginHookToolContext, |
| PluginHookBeforeToolCallEvent, |
| PluginHookBeforeToolCallResult, |
| PluginHookAfterToolCallEvent, |
| PluginHookToolResultPersistContext, |
| PluginHookToolResultPersistEvent, |
| PluginHookToolResultPersistResult, |
| PluginHookBeforeMessageWriteEvent, |
| PluginHookBeforeMessageWriteResult, |
| PluginHookSessionContext, |
| PluginHookSessionStartEvent, |
| PluginHookSessionEndEvent, |
| PluginHookSubagentContext, |
| PluginHookSubagentDeliveryTargetEvent, |
| PluginHookSubagentDeliveryTargetResult, |
| PluginHookSubagentSpawningEvent, |
| PluginHookSubagentSpawningResult, |
| PluginHookSubagentSpawnedEvent, |
| PluginHookSubagentEndedEvent, |
| 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; |
|
|
| const mergeBeforeModelResolve = ( |
| acc: PluginHookBeforeModelResolveResult | undefined, |
| next: PluginHookBeforeModelResolveResult, |
| ): PluginHookBeforeModelResolveResult => ({ |
| |
| modelOverride: acc?.modelOverride ?? next.modelOverride, |
| providerOverride: acc?.providerOverride ?? next.providerOverride, |
| }); |
|
|
| const mergeBeforePromptBuild = ( |
| acc: PluginHookBeforePromptBuildResult | undefined, |
| next: PluginHookBeforePromptBuildResult, |
| ): PluginHookBeforePromptBuildResult => ({ |
| systemPrompt: next.systemPrompt ?? acc?.systemPrompt, |
| prependContext: concatOptionalTextSegments({ |
| left: acc?.prependContext, |
| right: next.prependContext, |
| }), |
| prependSystemContext: concatOptionalTextSegments({ |
| left: acc?.prependSystemContext, |
| right: next.prependSystemContext, |
| }), |
| appendSystemContext: concatOptionalTextSegments({ |
| left: acc?.appendSystemContext, |
| right: next.appendSystemContext, |
| }), |
| }); |
|
|
| const mergeSubagentSpawningResult = ( |
| acc: PluginHookSubagentSpawningResult | undefined, |
| next: PluginHookSubagentSpawningResult, |
| ): PluginHookSubagentSpawningResult => { |
| if (acc?.status === "error") { |
| return acc; |
| } |
| if (next.status === "error") { |
| return next; |
| } |
| return { |
| status: "ok", |
| threadBindingReady: Boolean(acc?.threadBindingReady || next.threadBindingReady), |
| }; |
| }; |
|
|
| const mergeSubagentDeliveryTargetResult = ( |
| acc: PluginHookSubagentDeliveryTargetResult | undefined, |
| next: PluginHookSubagentDeliveryTargetResult, |
| ): PluginHookSubagentDeliveryTargetResult => { |
| if (acc?.origin) { |
| return acc; |
| } |
| return next; |
| }; |
|
|
| const handleHookError = (params: { |
| hookName: PluginHookName; |
| pluginId: string; |
| error: unknown; |
| }): never | void => { |
| const msg = `[hooks] ${params.hookName} handler from ${params.pluginId} failed: ${String( |
| params.error, |
| )}`; |
| if (catchErrors) { |
| logger?.error(msg); |
| return; |
| } |
| throw new Error(msg, { cause: params.error }); |
| }; |
|
|
| |
| |
| |
| |
| 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) { |
| handleHookError({ hookName, pluginId: hook.pluginId, error: 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) { |
| handleHookError({ hookName, pluginId: hook.pluginId, error: err }); |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| async function runBeforeModelResolve( |
| event: PluginHookBeforeModelResolveEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<PluginHookBeforeModelResolveResult | undefined> { |
| return runModifyingHook<"before_model_resolve", PluginHookBeforeModelResolveResult>( |
| "before_model_resolve", |
| event, |
| ctx, |
| mergeBeforeModelResolve, |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runBeforePromptBuild( |
| event: PluginHookBeforePromptBuildEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<PluginHookBeforePromptBuildResult | undefined> { |
| return runModifyingHook<"before_prompt_build", PluginHookBeforePromptBuildResult>( |
| "before_prompt_build", |
| event, |
| ctx, |
| mergeBeforePromptBuild, |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runBeforeAgentStart( |
| event: PluginHookBeforeAgentStartEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<PluginHookBeforeAgentStartResult | undefined> { |
| return runModifyingHook<"before_agent_start", PluginHookBeforeAgentStartResult>( |
| "before_agent_start", |
| event, |
| ctx, |
| (acc, next) => ({ |
| ...mergeBeforePromptBuild(acc, next), |
| ...mergeBeforeModelResolve(acc, next), |
| }), |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| async function runAgentEnd( |
| event: PluginHookAgentEndEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<void> { |
| return runVoidHook("agent_end", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| |
| async function runLlmInput(event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) { |
| return runVoidHook("llm_input", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| |
| async function runLlmOutput(event: PluginHookLlmOutputEvent, ctx: PluginHookAgentContext) { |
| return runVoidHook("llm_output", 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 runBeforeReset( |
| event: PluginHookBeforeResetEvent, |
| ctx: PluginHookAgentContext, |
| ): Promise<void> { |
| return runVoidHook("before_reset", 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 }; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function runBeforeMessageWrite( |
| event: PluginHookBeforeMessageWriteEvent, |
| ctx: { agentId?: string; sessionKey?: string }, |
| ): PluginHookBeforeMessageWriteResult | undefined { |
| const hooks = getHooksForName(registry, "before_message_write"); |
| 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 |
| | PluginHookBeforeMessageWriteResult |
| | void |
| | Promise<unknown>; |
|
|
| |
| |
| if (out && typeof (out as any).then === "function") { |
| const msg = |
| `[hooks] before_message_write 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 result = out as PluginHookBeforeMessageWriteResult | undefined; |
|
|
| |
| if (result?.block) { |
| return { block: true }; |
| } |
|
|
| |
| if (result?.message) { |
| current = result.message; |
| } |
| } catch (err) { |
| const msg = `[hooks] before_message_write handler from ${hook.pluginId} failed: ${String(err)}`; |
| if (catchErrors) { |
| logger?.error(msg); |
| } else { |
| throw new Error(msg, { cause: err }); |
| } |
| } |
| } |
|
|
| |
| if (current !== event.message) { |
| return { message: current }; |
| } |
|
|
| return undefined; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| 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 runSubagentSpawning( |
| event: PluginHookSubagentSpawningEvent, |
| ctx: PluginHookSubagentContext, |
| ): Promise<PluginHookSubagentSpawningResult | undefined> { |
| return runModifyingHook<"subagent_spawning", PluginHookSubagentSpawningResult>( |
| "subagent_spawning", |
| event, |
| ctx, |
| mergeSubagentSpawningResult, |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runSubagentDeliveryTarget( |
| event: PluginHookSubagentDeliveryTargetEvent, |
| ctx: PluginHookSubagentContext, |
| ): Promise<PluginHookSubagentDeliveryTargetResult | undefined> { |
| return runModifyingHook<"subagent_delivery_target", PluginHookSubagentDeliveryTargetResult>( |
| "subagent_delivery_target", |
| event, |
| ctx, |
| mergeSubagentDeliveryTargetResult, |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function runSubagentSpawned( |
| event: PluginHookSubagentSpawnedEvent, |
| ctx: PluginHookSubagentContext, |
| ): Promise<void> { |
| return runVoidHook("subagent_spawned", event, ctx); |
| } |
|
|
| |
| |
| |
| |
| async function runSubagentEnded( |
| event: PluginHookSubagentEndedEvent, |
| ctx: PluginHookSubagentContext, |
| ): Promise<void> { |
| return runVoidHook("subagent_ended", 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 { |
| |
| runBeforeModelResolve, |
| runBeforePromptBuild, |
| runBeforeAgentStart, |
| runLlmInput, |
| runLlmOutput, |
| runAgentEnd, |
| runBeforeCompaction, |
| runAfterCompaction, |
| runBeforeReset, |
| |
| runMessageReceived, |
| runMessageSending, |
| runMessageSent, |
| |
| runBeforeToolCall, |
| runAfterToolCall, |
| runToolResultPersist, |
| |
| runBeforeMessageWrite, |
| |
| runSessionStart, |
| runSessionEnd, |
| runSubagentSpawning, |
| runSubagentDeliveryTarget, |
| runSubagentSpawned, |
| runSubagentEnded, |
| |
| runGatewayStart, |
| runGatewayStop, |
| |
| hasHooks, |
| getHookCount, |
| }; |
| } |
|
|
| export type HookRunner = ReturnType<typeof createHookRunner>; |
|
|