mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 14:00:51 +00:00
feat: add search provider lifecycle hooks
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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(
|
||||
|
||||
@@ -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<PluginHookBeforeSearchProviderConfigureResult | undefined> {
|
||||
return runModifyingHook<
|
||||
"before_search_provider_configure",
|
||||
PluginHookBeforeSearchProviderConfigureResult
|
||||
>("before_search_provider_configure", event, ctx, mergeBeforeSearchProviderConfigure);
|
||||
}
|
||||
|
||||
async function runAfterSearchProviderConfigure(
|
||||
event: PluginHookAfterSearchProviderConfigureEvent,
|
||||
ctx: PluginHookSearchProviderContext,
|
||||
): Promise<void> {
|
||||
return runVoidHook("after_search_provider_configure", event, ctx);
|
||||
}
|
||||
|
||||
async function runAfterSearchProviderActivate(
|
||||
event: PluginHookAfterSearchProviderActivateEvent,
|
||||
ctx: PluginHookSearchProviderContext,
|
||||
): Promise<void> {
|
||||
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,
|
||||
|
||||
@@ -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> | PluginHookBeforeAgentStartResult | void;
|
||||
before_search_provider_configure: (
|
||||
event: PluginHookBeforeSearchProviderConfigureEvent,
|
||||
ctx: PluginHookSearchProviderContext,
|
||||
) =>
|
||||
| Promise<PluginHookBeforeSearchProviderConfigureResult | void>
|
||||
| PluginHookBeforeSearchProviderConfigureResult
|
||||
| void;
|
||||
after_search_provider_configure: (
|
||||
event: PluginHookAfterSearchProviderConfigureEvent,
|
||||
ctx: PluginHookSearchProviderContext,
|
||||
) => Promise<void> | void;
|
||||
after_search_provider_activate: (
|
||||
event: PluginHookAfterSearchProviderActivateEvent,
|
||||
ctx: PluginHookSearchProviderContext,
|
||||
) => Promise<void> | void;
|
||||
llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
|
||||
llm_output: (
|
||||
event: PluginHookLlmOutputEvent,
|
||||
|
||||
Reference in New Issue
Block a user