diff --git a/src/commands/onboard-search.test.ts b/src/commands/onboard-search.test.ts index f52839551c4..0c710202eb5 100644 --- a/src/commands/onboard-search.test.ts +++ b/src/commands/onboard-search.test.ts @@ -4,7 +4,7 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; const loadOpenClawPlugins = vi.hoisted(() => - vi.fn(() => ({ searchProviders: [] as unknown[], plugins: [] as unknown[] })), + vi.fn(() => ({ searchProviders: [] as unknown[], plugins: [] as unknown[], typedHooks: [] })), ); const loadPluginManifestRegistry = vi.hoisted(() => vi.fn(() => ({ plugins: [] as unknown[], diagnostics: [] as unknown[] })), @@ -116,7 +116,7 @@ describe("setupSearch", () => { vi.stubEnv("MOONSHOT_API_KEY", ""); vi.stubEnv("PERPLEXITY_API_KEY", ""); loadOpenClawPlugins.mockReset(); - loadOpenClawPlugins.mockReturnValue({ searchProviders: [], plugins: [] }); + loadOpenClawPlugins.mockReturnValue({ searchProviders: [], plugins: [], typedHooks: [] }); loadPluginManifestRegistry.mockReset(); loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] }); ensureOnboardingPluginInstalled.mockReset(); @@ -154,6 +154,7 @@ describe("setupSearch", () => { configUiHints: undefined, }, ], + typedHooks: [], }); const cfg: OpenClawConfig = {}; @@ -257,6 +258,7 @@ describe("setupSearch", () => { configUiHints: undefined, }, ], + typedHooks: [], }); const cfg: OpenClawConfig = { tools: { @@ -384,6 +386,7 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], }); const cfg: OpenClawConfig = {}; @@ -411,6 +414,151 @@ describe("setupSearch", () => { ); }); + it("shows a provider setup note from before_search_provider_configure hooks", async () => { + loadOpenClawPlugins.mockReturnValue({ + searchProviders: [ + { + pluginId: "tavily-search", + provider: { + id: "tavily", + name: "Tavily Search", + description: "Plugin search", + configFieldOrder: ["apiKey"], + search: async () => ({ content: "ok" }), + }, + }, + ], + plugins: [ + { + id: "tavily-search", + name: "Tavily Search", + description: "External Tavily plugin", + origin: "workspace", + source: "/tmp/tavily-search", + configJsonSchema: { + type: "object", + properties: { + apiKey: { type: "string" }, + }, + }, + configUiHints: { + apiKey: { + label: "Tavily API key", + placeholder: "tvly-...", + sensitive: true, + }, + }, + }, + ], + typedHooks: [ + { + pluginId: "tavily-search", + hookName: "before_search_provider_configure", + priority: 0, + source: "/tmp/tavily-search", + handler: () => ({ note: "Read the provider docs before entering your key." }), + }, + ], + }); + + const { prompter, notes } = createPrompter({ + actionValue: "__configure_provider__", + selectValue: "tavily", + textValue: "tvly-test-key", + }); + + await setupSearch({}, runtime, prompter); + + expect(notes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + title: "Provider setup", + message: "Read the provider docs before entering your key.", + }), + ]), + ); + }); + + it("fires after_search_provider_activate only when the active provider changes", async () => { + const afterActivate = vi.fn(); + loadOpenClawPlugins.mockReturnValue({ + searchProviders: [ + { + pluginId: "tavily-search", + provider: { + id: "tavily", + name: "Tavily Search", + description: "Plugin search", + isAvailable: () => true, + search: async () => ({ content: "ok" }), + }, + }, + ], + plugins: [ + { + id: "tavily-search", + name: "Tavily Search", + description: "External Tavily plugin", + origin: "workspace", + source: "/tmp/tavily-search", + configJsonSchema: undefined, + configUiHints: undefined, + }, + ], + typedHooks: [ + { + pluginId: "tavily-search", + hookName: "after_search_provider_activate", + priority: 0, + source: "/tmp/tavily-search", + handler: afterActivate, + }, + ], + }); + + const cfg: OpenClawConfig = { + tools: { + web: { + search: { + provider: "brave", + enabled: true, + apiKey: "BSA-test-key", + }, + }, + }, + plugins: { + entries: { + "tavily-search": { + enabled: true, + config: { + apiKey: "tvly-existing-key", + }, + }, + }, + }, + }; + + const { prompter } = createPrompter({ + actionValue: "__switch_active__", + selectValue: "tavily", + }); + + const result = await setupSearch(cfg, runtime, prompter); + + expect(result.tools?.web?.search?.provider).toBe("tavily"); + expect(afterActivate).toHaveBeenCalledTimes(1); + expect(afterActivate).toHaveBeenCalledWith( + expect.objectContaining({ + providerId: "tavily", + previousProviderId: "brave", + intent: "switch-active", + }), + expect.objectContaining({ + workspaceDir: undefined, + }), + ); + }); + it("re-prompts invalid plugin config values before saving", async () => { loadOpenClawPlugins.mockReturnValue({ searchProviders: [ @@ -452,6 +600,7 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], }); const cfg: OpenClawConfig = {}; @@ -517,6 +666,7 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], }); const cfg: OpenClawConfig = { @@ -600,8 +750,9 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], } - : { searchProviders: [], plugins: [] }; + : { searchProviders: [], plugins: [], typedHooks: [] }; }); ensureOnboardingPluginInstalled.mockImplementation( async ({ cfg }: { cfg: OpenClawConfig }) => ({ @@ -703,6 +854,7 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], } : { searchProviders: [], @@ -733,6 +885,7 @@ describe("setupSearch", () => { }, }, ], + typedHooks: [], }; }); loadPluginManifestRegistry.mockReturnValue({ diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index 0638f517f35..9fc1b7f08c8 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -17,6 +17,7 @@ import { resolveCapabilitySlotSelection, } from "../plugins/capability-slots.js"; import { enablePluginInConfig } from "../plugins/enable.js"; +import { createHookRunner, type HookRunner } from "../plugins/hooks.js"; import { loadOpenClawPlugins } from "../plugins/loader.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { validateJsonSchemaValue } from "../plugins/schema-validator.js"; @@ -96,6 +97,19 @@ type PluginPromptableField = existingValue?: boolean; }; +type SearchProviderHookDetails = { + providerId: string; + providerLabel: string; + providerSource: "builtin" | "plugin"; + pluginId?: string; + configured: boolean; +}; + +const HOOK_RUNNER_LOGGER = { + warn: () => {}, + error: () => {}, +} as const; + function hasNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } @@ -285,6 +299,113 @@ function validatePluginSearchProviderConfig( }; } +function createSearchProviderHookRunner( + config: OpenClawConfig, + workspaceDir?: string, +): HookRunner | null { + try { + const registry = loadOpenClawPlugins({ + config, + cache: false, + workspaceDir, + suppressOpenAllowlistWarning: true, + }); + if (registry.typedHooks.length === 0) { + return null; + } + return createHookRunner(registry, { + logger: HOOK_RUNNER_LOGGER, + catchErrors: true, + }); + } catch { + return null; + } +} + +async function maybeNoteBeforeSearchProviderConfigure(params: { + hookRunner: HookRunner | null; + config: OpenClawConfig; + provider: SearchProviderHookDetails; + intent: SearchProviderFlowIntent; + prompter: WizardPrompter; + workspaceDir?: string; +}): Promise { + if (!params.hookRunner?.hasHooks("before_search_provider_configure")) { + return; + } + const result = await params.hookRunner.runBeforeSearchProviderConfigure( + { + providerId: params.provider.providerId, + providerLabel: params.provider.providerLabel, + providerSource: params.provider.providerSource, + pluginId: params.provider.pluginId, + intent: params.intent, + activeProviderId: resolveCapabilitySlotSelection(params.config, "providers.search") ?? null, + configured: params.provider.configured, + }, + { + workspaceDir: params.workspaceDir, + }, + ); + if (result?.note?.trim()) { + await params.prompter.note(result.note, "Provider setup"); + } +} + +async function runAfterSearchProviderHooks(params: { + hookRunner: HookRunner | null; + originalConfig: OpenClawConfig; + resultConfig: OpenClawConfig; + provider: SearchProviderHookDetails; + intent: SearchProviderFlowIntent; + workspaceDir?: string; +}): Promise { + if (!params.hookRunner) { + return; + } + const activeProviderBefore = + resolveCapabilitySlotSelection(params.originalConfig, "providers.search") ?? null; + const activeProviderAfter = + resolveCapabilitySlotSelection(params.resultConfig, "providers.search") ?? null; + + if (params.hookRunner.hasHooks("after_search_provider_configure")) { + await params.hookRunner.runAfterSearchProviderConfigure( + { + providerId: params.provider.providerId, + providerLabel: params.provider.providerLabel, + providerSource: params.provider.providerSource, + pluginId: params.provider.pluginId, + intent: params.intent, + activeProviderId: activeProviderAfter, + configured: params.provider.configured, + }, + { + workspaceDir: params.workspaceDir, + }, + ); + } + + if ( + activeProviderAfter === params.provider.providerId && + activeProviderBefore !== activeProviderAfter && + params.hookRunner.hasHooks("after_search_provider_activate") + ) { + await params.hookRunner.runAfterSearchProviderActivate( + { + providerId: params.provider.providerId, + providerLabel: params.provider.providerLabel, + providerSource: params.provider.providerSource, + pluginId: params.provider.pluginId, + previousProviderId: activeProviderBefore, + intent: params.intent, + }, + { + workspaceDir: params.workspaceDir, + }, + ); + } +} + async function promptPluginSearchProviderConfig( config: OpenClawConfig, entry: PluginSearchProviderEntry, @@ -610,12 +731,42 @@ export async function applySearchProviderChoice(params: { return installedConfig; } const enabled = enablePluginInConfig(installedConfig, installedProvider.pluginId); + const hookRunner = createSearchProviderHookRunner(enabled.config, params.opts?.workspaceDir); + const providerDetails: SearchProviderHookDetails = { + providerId: installedProvider.value, + providerLabel: installedProvider.label, + providerSource: "plugin", + pluginId: installedProvider.pluginId, + configured: installedProvider.configured, + }; let next = intent === "switch-active" ? setWebSearchProvider(enabled.config, installedProvider.value) : enabled.config; + await maybeNoteBeforeSearchProviderConfigure({ + hookRunner, + config: next, + provider: providerDetails, + intent, + prompter: params.prompter, + workspaceDir: params.opts?.workspaceDir, + }); next = await promptPluginSearchProviderConfig(next, installedProvider, params.prompter); - return preserveSearchProviderIntent(installedConfig, next, intent, installedProvider.value); + const result = preserveSearchProviderIntent( + installedConfig, + next, + intent, + installedProvider.value, + ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: installedConfig, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: params.opts?.workspaceDir, + }); + return result; } return configureSearchProviderSelection( @@ -775,18 +926,61 @@ export async function configureSearchProviderSelection( const selectedEntry = providerEntries.find((entry) => entry.value === choice); if (selectedEntry?.kind === "plugin") { const enabled = enablePluginInConfig(config, selectedEntry.pluginId); + const hookRunner = createSearchProviderHookRunner(enabled.config, opts?.workspaceDir); + const providerDetails: SearchProviderHookDetails = { + providerId: selectedEntry.value, + providerLabel: selectedEntry.label, + providerSource: "plugin", + pluginId: selectedEntry.pluginId, + configured: selectedEntry.configured, + }; let next = intent === "switch-active" ? setWebSearchProvider(enabled.config, selectedEntry.value) : enabled.config; if (selectedEntry.configured) { - return preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + const result = preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } if (opts?.quickstartDefaults && selectedEntry.configured) { - return preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + const result = preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } + await maybeNoteBeforeSearchProviderConfigure({ + hookRunner, + config: next, + provider: providerDetails, + intent, + prompter, + workspaceDir: opts?.workspaceDir, + }); next = await promptPluginSearchProviderConfig(next, selectedEntry, prompter); - return preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + const result = preserveSearchProviderIntent(config, next, intent, selectedEntry.value); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } const builtinChoice = choice as SearchProvider; @@ -794,6 +988,13 @@ export async function configureSearchProviderSelection( if (!entry) { return config; } + const hookRunner = createSearchProviderHookRunner(config, opts?.workspaceDir); + const providerDetails: SearchProviderHookDetails = { + providerId: builtinChoice, + providerLabel: entry.label, + providerSource: "builtin", + configured: hasExistingKey(config, builtinChoice) || hasKeyInEnv(entry), + }; const existingKey = resolveExistingKey(config, builtinChoice); const keyConfigured = hasExistingKey(config, builtinChoice); const envAvailable = hasKeyInEnv(entry); @@ -802,16 +1003,43 @@ export async function configureSearchProviderSelection( const result = existingKey ? applySearchKey(config, builtinChoice, existingKey) : applyProviderOnly(config, builtinChoice); - return preserveSearchProviderIntent(config, result, intent, builtinChoice); + const next = preserveSearchProviderIntent(config, result, intent, builtinChoice); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: next, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return next; } if (opts?.quickstartDefaults && (keyConfigured || envAvailable)) { const result = existingKey ? applySearchKey(config, builtinChoice, existingKey) : applyProviderOnly(config, builtinChoice); - return preserveSearchProviderIntent(config, result, intent, builtinChoice); + const next = preserveSearchProviderIntent(config, result, intent, builtinChoice); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: next, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return next; } + await maybeNoteBeforeSearchProviderConfigure({ + hookRunner, + config, + provider: providerDetails, + intent, + prompter, + workspaceDir: opts?.workspaceDir, + }); + const useSecretRefMode = opts?.secretInputMode === "ref"; // pragma: allowlist secret if (useSecretRefMode) { if (keyConfigured) { @@ -827,12 +1055,21 @@ export async function configureSearchProviderSelection( ].join("\n"), "Web search", ); - return preserveSearchProviderIntent( + const result = preserveSearchProviderIntent( config, applySearchKey(config, builtinChoice, ref), intent, builtinChoice, ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } const keyInput = await prompter.text({ @@ -847,30 +1084,57 @@ export async function configureSearchProviderSelection( const key = keyInput?.trim() ?? ""; if (key) { const secretInput = resolveSearchSecretInput(builtinChoice, key, opts?.secretInputMode); - return preserveSearchProviderIntent( + const result = preserveSearchProviderIntent( config, applySearchKey(config, builtinChoice, secretInput), intent, builtinChoice, ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } if (existingKey) { - return preserveSearchProviderIntent( + const result = preserveSearchProviderIntent( config, applySearchKey(config, builtinChoice, existingKey), intent, builtinChoice, ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } if (keyConfigured || envAvailable) { - return preserveSearchProviderIntent( + const result = preserveSearchProviderIntent( config, applyProviderOnly(config, builtinChoice), intent, builtinChoice, ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } await prompter.note( @@ -882,7 +1146,7 @@ export async function configureSearchProviderSelection( "Web search", ); - return preserveSearchProviderIntent( + const result = preserveSearchProviderIntent( config, applyCapabilitySlotSelection({ config, @@ -892,6 +1156,15 @@ export async function configureSearchProviderSelection( intent, builtinChoice, ); + await runAfterSearchProviderHooks({ + hookRunner, + originalConfig: config, + resultConfig: result, + provider: providerDetails, + intent, + workspaceDir: opts?.workspaceDir, + }); + return result; } function preserveSearchProviderIntent( diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 4d74267d4ca..c862ae17169 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -8,12 +8,16 @@ import { concatOptionalTextSegments } from "../shared/text/join-segments.js"; import type { PluginRegistry } from "./registry.js"; import type { + PluginHookAfterSearchProviderActivateEvent, + PluginHookAfterSearchProviderConfigureEvent, PluginHookAfterCompactionEvent, PluginHookAfterToolCallEvent, PluginHookAgentContext, PluginHookAgentEndEvent, PluginHookBeforeAgentStartEvent, PluginHookBeforeAgentStartResult, + PluginHookBeforeSearchProviderConfigureEvent, + PluginHookBeforeSearchProviderConfigureResult, PluginHookBeforeModelResolveEvent, PluginHookBeforeModelResolveResult, PluginHookBeforePromptBuildEvent, @@ -37,6 +41,7 @@ import type { PluginHookSessionContext, PluginHookSessionEndEvent, PluginHookSessionStartEvent, + PluginHookSearchProviderContext, PluginHookSubagentContext, PluginHookSubagentDeliveryTargetEvent, PluginHookSubagentDeliveryTargetResult, @@ -57,6 +62,8 @@ export type { PluginHookAgentContext, PluginHookBeforeAgentStartEvent, PluginHookBeforeAgentStartResult, + PluginHookBeforeSearchProviderConfigureEvent, + PluginHookBeforeSearchProviderConfigureResult, PluginHookBeforeModelResolveEvent, PluginHookBeforeModelResolveResult, PluginHookBeforePromptBuildEvent, @@ -84,6 +91,7 @@ export type { PluginHookSessionContext, PluginHookSessionStartEvent, PluginHookSessionEndEvent, + PluginHookSearchProviderContext, PluginHookSubagentContext, PluginHookSubagentDeliveryTargetEvent, PluginHookSubagentDeliveryTargetResult, @@ -94,6 +102,8 @@ export type { PluginHookGatewayContext, PluginHookGatewayStartEvent, PluginHookGatewayStopEvent, + PluginHookAfterSearchProviderConfigureEvent, + PluginHookAfterSearchProviderActivateEvent, }; export type HookRunnerLogger = { @@ -181,6 +191,16 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp return next; }; + const mergeBeforeSearchProviderConfigure = ( + acc: PluginHookBeforeSearchProviderConfigureResult | undefined, + next: PluginHookBeforeSearchProviderConfigureResult, + ): PluginHookBeforeSearchProviderConfigureResult => ({ + note: concatOptionalTextSegments({ + left: acc?.note, + right: next.note, + }), + }); + const handleHookError = (params: { hookName: PluginHookName; pluginId: string; @@ -318,6 +338,30 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp ); } + async function runBeforeSearchProviderConfigure( + event: PluginHookBeforeSearchProviderConfigureEvent, + ctx: PluginHookSearchProviderContext, + ): Promise { + return runModifyingHook< + "before_search_provider_configure", + PluginHookBeforeSearchProviderConfigureResult + >("before_search_provider_configure", event, ctx, mergeBeforeSearchProviderConfigure); + } + + async function runAfterSearchProviderConfigure( + event: PluginHookAfterSearchProviderConfigureEvent, + ctx: PluginHookSearchProviderContext, + ): Promise { + return runVoidHook("after_search_provider_configure", event, ctx); + } + + async function runAfterSearchProviderActivate( + event: PluginHookAfterSearchProviderActivateEvent, + ctx: PluginHookSearchProviderContext, + ): Promise { + return runVoidHook("after_search_provider_activate", event, ctx); + } + /** * Run agent_end hook. * Allows plugins to analyze completed conversations. @@ -727,6 +771,9 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp runBeforeModelResolve, runBeforePromptBuild, runBeforeAgentStart, + runBeforeSearchProviderConfigure, + runAfterSearchProviderConfigure, + runAfterSearchProviderActivate, runLlmInput, runLlmOutput, runAgentEnd, diff --git a/src/plugins/types.ts b/src/plugins/types.ts index d69bffc83d1..6c8268124dd 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -502,6 +502,9 @@ export type PluginHookName = | "before_model_resolve" | "before_prompt_build" | "before_agent_start" + | "before_search_provider_configure" + | "after_search_provider_configure" + | "after_search_provider_activate" | "llm_input" | "llm_output" | "agent_end" @@ -528,6 +531,9 @@ export const PLUGIN_HOOK_NAMES = [ "before_model_resolve", "before_prompt_build", "before_agent_start", + "before_search_provider_configure", + "after_search_provider_configure", + "after_search_provider_activate", "llm_input", "llm_output", "agent_end", @@ -667,6 +673,46 @@ export const stripPromptMutationFieldsFromLegacyHookResult = ( : undefined; }; +// search-provider hooks +export type PluginHookSearchProviderContext = { + workspaceDir?: string; +}; + +export type PluginHookSearchProviderSource = "builtin" | "plugin"; + +export type PluginHookBeforeSearchProviderConfigureEvent = { + providerId: string; + providerLabel: string; + providerSource: PluginHookSearchProviderSource; + pluginId?: string; + intent: "switch-active" | "configure-provider"; + activeProviderId?: string | null; + configured: boolean; +}; + +export type PluginHookBeforeSearchProviderConfigureResult = { + note?: string; +}; + +export type PluginHookAfterSearchProviderConfigureEvent = { + providerId: string; + providerLabel: string; + providerSource: PluginHookSearchProviderSource; + pluginId?: string; + intent: "switch-active" | "configure-provider"; + activeProviderId?: string | null; + configured: boolean; +}; + +export type PluginHookAfterSearchProviderActivateEvent = { + providerId: string; + providerLabel: string; + providerSource: PluginHookSearchProviderSource; + pluginId?: string; + previousProviderId?: string | null; + intent: "switch-active" | "configure-provider"; +}; + // llm_input hook export type PluginHookLlmInputEvent = { runId: string; @@ -980,6 +1026,21 @@ export type PluginHookHandlerMap = { event: PluginHookBeforeAgentStartEvent, ctx: PluginHookAgentContext, ) => Promise | PluginHookBeforeAgentStartResult | void; + before_search_provider_configure: ( + event: PluginHookBeforeSearchProviderConfigureEvent, + ctx: PluginHookSearchProviderContext, + ) => + | Promise + | PluginHookBeforeSearchProviderConfigureResult + | void; + after_search_provider_configure: ( + event: PluginHookAfterSearchProviderConfigureEvent, + ctx: PluginHookSearchProviderContext, + ) => Promise | void; + after_search_provider_activate: ( + event: PluginHookAfterSearchProviderActivateEvent, + ctx: PluginHookSearchProviderContext, + ) => Promise | void; llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise | void; llm_output: ( event: PluginHookLlmOutputEvent,