diff --git a/src/image-generation/provider-registry.test.ts b/src/image-generation/provider-registry.test.ts index 2183630aaa8..94164b70ffc 100644 --- a/src/image-generation/provider-registry.test.ts +++ b/src/image-generation/provider-registry.test.ts @@ -2,12 +2,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; -const { loadOpenClawPluginsMock } = vi.hoisted(() => ({ +const { loadOpenClawPluginsMock, getCompatibleActivePluginRegistryMock } = vi.hoisted(() => ({ loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()), + getCompatibleActivePluginRegistryMock: vi.fn< + (params?: unknown) => ReturnType | undefined + >(() => undefined), })); vi.mock("../plugins/loader.js", () => ({ loadOpenClawPlugins: loadOpenClawPluginsMock, + getCompatibleActivePluginRegistry: getCompatibleActivePluginRegistryMock, })); let getImageGenerationProvider: typeof import("./provider-registry.js").getImageGenerationProvider; @@ -17,6 +21,8 @@ describe("image-generation provider registry", () => { afterEach(() => { loadOpenClawPluginsMock.mockReset(); loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry()); + getCompatibleActivePluginRegistryMock.mockReset(); + getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); resetPluginRuntimeStateForTest(); }); @@ -50,6 +56,7 @@ describe("image-generation provider registry", () => { }, }); setActivePluginRegistry(registry); + getCompatibleActivePluginRegistryMock.mockReturnValue(registry); const provider = getImageGenerationProvider("custom-image"); @@ -94,6 +101,7 @@ describe("image-generation provider registry", () => { }, ); setActivePluginRegistry(registry); + getCompatibleActivePluginRegistryMock.mockReturnValue(registry); expect(listImageGenerationProviders().map((provider) => provider.id)).toEqual(["safe-image"]); expect(getImageGenerationProvider("__proto__")).toBeUndefined(); diff --git a/src/image-generation/provider-registry.ts b/src/image-generation/provider-registry.ts index 0d90fc31edc..9c16a7a345f 100644 --- a/src/image-generation/provider-registry.ts +++ b/src/image-generation/provider-registry.ts @@ -2,7 +2,6 @@ import { normalizeProviderId } from "../agents/model-selection.js"; import type { OpenClawConfig } from "../config/config.js"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js"; -import { getActivePluginRegistryKey } from "../plugins/runtime.js"; import type { ImageGenerationProviderPlugin } from "../plugins/types.js"; const BUILTIN_IMAGE_GENERATION_PROVIDERS: readonly ImageGenerationProviderPlugin[] = []; @@ -26,8 +25,6 @@ function resolvePluginImageGenerationProviders( return resolvePluginCapabilityProviders({ key: "imageGenerationProviders", cfg, - useActiveRegistryWhen: (active) => - (active?.imageGenerationProviders?.length ?? 0) > 0 || Boolean(getActivePluginRegistryKey()), }); } diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index 7ae2edd1c93..609416511b0 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "./registry.js"; -import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js"; +import { resetPluginRuntimeStateForTest } from "./runtime.js"; type MockManifestRegistry = { plugins: Array>; @@ -14,6 +14,9 @@ function createEmptyMockManifestRegistry(): MockManifestRegistry { const mocks = vi.hoisted(() => ({ loadOpenClawPlugins: vi.fn(() => createEmptyPluginRegistry()), + getCompatibleActivePluginRegistry: vi.fn< + (params?: unknown) => ReturnType | undefined + >(() => undefined), loadPluginManifestRegistry: vi.fn<() => MockManifestRegistry>(() => createEmptyMockManifestRegistry(), ), @@ -24,6 +27,7 @@ const mocks = vi.hoisted(() => ({ vi.mock("./loader.js", () => ({ loadOpenClawPlugins: mocks.loadOpenClawPlugins, + getCompatibleActivePluginRegistry: mocks.getCompatibleActivePluginRegistry, })); vi.mock("./manifest-registry.js", () => ({ @@ -104,33 +108,14 @@ function setBundledCapabilityFixture(contractKey: string) { }); } -function setActiveSpeechCapabilityRegistry(providerId: string) { - const active = createEmptyPluginRegistry(); - active.speechProviders.push({ - pluginId: providerId, - pluginName: "OpenAI", - source: "test", - provider: { - id: providerId, - label: "OpenAI", - isConfigured: () => true, - synthesize: async () => ({ - audioBuffer: Buffer.from("x"), - outputFormat: "mp3", - voiceCompatible: false, - fileExtension: ".mp3", - }), - }, - }); - setActivePluginRegistry(active); -} - describe("resolvePluginCapabilityProviders", () => { beforeEach(async () => { vi.resetModules(); resetPluginRuntimeStateForTest(); mocks.loadOpenClawPlugins.mockReset(); mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry()); + mocks.getCompatibleActivePluginRegistry.mockReset(); + mocks.getCompatibleActivePluginRegistry.mockReturnValue(undefined); mocks.loadPluginManifestRegistry.mockReset(); mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry()); mocks.withBundledPluginAllowlistCompat.mockReset(); @@ -143,7 +128,24 @@ describe("resolvePluginCapabilityProviders", () => { }); it("uses the active registry when capability providers are already loaded", () => { - setActiveSpeechCapabilityRegistry("openai"); + const active = createEmptyPluginRegistry(); + active.speechProviders.push({ + pluginId: "openai", + pluginName: "OpenAI", + source: "test", + provider: { + id: "openai", + label: "OpenAI", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.from("x"), + outputFormat: "mp3", + voiceCompatible: false, + fileExtension: ".mp3", + }), + }, + }); + mocks.getCompatibleActivePluginRegistry.mockReturnValue(active); const providers = resolvePluginCapabilityProviders({ key: "speechProviders" }); @@ -171,4 +173,17 @@ describe("resolvePluginCapabilityProviders", () => { enablementCompat, }); }); + + it("reuses a compatible active registry even when the capability list is empty", () => { + const active = createEmptyPluginRegistry(); + mocks.getCompatibleActivePluginRegistry.mockReturnValue(active); + + const providers = resolvePluginCapabilityProviders({ + key: "mediaUnderstandingProviders", + cfg: {} as OpenClawConfig, + }); + + expect(providers).toEqual([]); + expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 2c14e868956..4cd2586b1c1 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -4,10 +4,9 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "./bundled-compat.js"; -import { loadOpenClawPlugins } from "./loader.js"; +import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginRegistry } from "./registry.js"; -import { getActivePluginRegistry } from "./runtime.js"; type CapabilityProviderRegistryKey = | "speechProviders" @@ -67,17 +66,17 @@ function resolveCapabilityProviderConfig(params: { export function resolvePluginCapabilityProviders(params: { key: K; cfg?: OpenClawConfig; - useActiveRegistryWhen?: (active: PluginRegistry | undefined) => boolean; }): CapabilityProviderForKey[] { - const active = getActivePluginRegistry() ?? undefined; - const shouldUseActive = - params.useActiveRegistryWhen?.(active) ?? (active?.[params.key].length ?? 0) > 0; - const registry = - shouldUseActive || !params.cfg - ? active - : loadOpenClawPlugins({ + const loadOptions = + params.cfg === undefined + ? undefined + : { config: resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg }), - }); + }; + const registry = + (loadOptions ? getCompatibleActivePluginRegistry(loadOptions) : undefined) ?? + (loadOptions ? loadOpenClawPlugins(loadOptions) : undefined) ?? + getCompatibleActivePluginRegistry(); return (registry?.[params.key] ?? []).map( (entry) => entry.provider, ) as CapabilityProviderForKey[]; diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index d25c2fe9c00..7b938ea04ec 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -8,10 +8,13 @@ type MockRegistryToolEntry = { }; const loadOpenClawPluginsMock = vi.fn(); +const getCompatibleActivePluginRegistryMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); vi.mock("./loader.js", () => ({ loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params), + getCompatibleActivePluginRegistry: (params: unknown) => + getCompatibleActivePluginRegistryMock(params), })); vi.mock("../config/plugin-auto-enable.js", () => ({ @@ -20,7 +23,6 @@ vi.mock("../config/plugin-auto-enable.js", () => ({ let resolvePluginTools: typeof import("./tools.js").resolvePluginTools; let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest; -let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry; function makeTool(name: string) { return { @@ -135,13 +137,14 @@ describe("resolvePluginTools optional tools", () => { beforeEach(async () => { vi.resetModules(); loadOpenClawPluginsMock.mockClear(); + getCompatibleActivePluginRegistryMock.mockReset(); + getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); applyPluginAutoEnableMock.mockReset(); applyPluginAutoEnableMock.mockImplementation(({ config }: { config: unknown }) => ({ config, changes: [], })); ({ resetPluginRuntimeStateForTest } = await import("./runtime.js")); - ({ setActivePluginRegistry } = await import("./runtime.js")); resetPluginRuntimeStateForTest(); ({ resolvePluginTools } = await import("./tools.js")); }); @@ -289,31 +292,6 @@ describe("resolvePluginTools optional tools", () => { }, }; applyPluginAutoEnableMock.mockReturnValue({ config: autoEnabledConfig, changes: [] }); - setActivePluginRegistry( - { - plugins: [], - tools: [], - hooks: [], - typedHooks: [], - channels: [], - channelSetups: [], - providers: [], - cliBackends: [], - speechProviders: [], - mediaUnderstandingProviders: [], - imageGenerationProviders: [], - webSearchProviders: [], - gatewayHandlers: {}, - gatewayMethodScopes: {}, - httpRoutes: [], - cliRegistrars: [], - services: [], - commands: [], - conversationBindingResolvedHandlers: [], - diagnostics: [], - } as never, - "stale-registry", - ); const tools = resolvePluginTools({ context: { @@ -326,4 +304,28 @@ describe("resolvePluginTools optional tools", () => { expectResolvedToolNames(tools, ["optional_tool"]); expectLoaderCall({ config: autoEnabledConfig }); }); + + it("reuses a compatible active registry instead of loading again", () => { + const activeRegistry = { + tools: [ + { + pluginId: "optional-demo", + optional: true, + source: "/tmp/optional-demo.js", + factory: () => makeTool("optional_tool"), + }, + ], + diagnostics: [], + }; + getCompatibleActivePluginRegistryMock.mockReturnValue(activeRegistry); + + const tools = resolvePluginTools( + createResolveToolsParams({ + toolAllowlist: ["optional_tool"], + }), + ); + + expectResolvedToolNames(tools, ["optional_tool"]); + expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index a4cba1020d9..2e3bf0c0163 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -3,9 +3,8 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; -import { loadOpenClawPlugins } from "./loader.js"; +import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; -import { getActivePluginRegistry, getActivePluginRegistryKey } from "./runtime.js"; import type { OpenClawPluginToolContext } from "./types.js"; const log = createSubsystemLogger("plugins"); @@ -69,21 +68,19 @@ export function resolvePluginTools(params: { return []; } - const activeRegistry = getActivePluginRegistry(); + const loadOptions = { + config: effectiveConfig, + workspaceDir: params.context.workspaceDir, + runtimeOptions: params.allowGatewaySubagentBinding + ? { + allowGatewaySubagentBinding: true, + } + : undefined, + env, + logger: createPluginLoaderLogger(log), + }; const registry = - getActivePluginRegistryKey() && activeRegistry && effectiveConfig === baseConfig - ? activeRegistry - : loadOpenClawPlugins({ - config: effectiveConfig, - workspaceDir: params.context.workspaceDir, - runtimeOptions: params.allowGatewaySubagentBinding - ? { - allowGatewaySubagentBinding: true, - } - : undefined, - env, - logger: createPluginLoaderLogger(log), - }); + getCompatibleActivePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions); const tools: AnyAgentTool[] = []; const existing = params.existingToolNames ?? new Set(); diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 69e41f3f6a8..56e444bc642 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -5,10 +5,14 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plug import type { SpeechProviderPlugin } from "../plugins/types.js"; const loadOpenClawPluginsMock = vi.fn(); +const getCompatibleActivePluginRegistryMock = vi.fn(); vi.mock("../plugins/loader.js", () => ({ loadOpenClawPlugins: (...args: Parameters) => loadOpenClawPluginsMock(...args), + getCompatibleActivePluginRegistry: ( + ...args: Parameters + ) => getCompatibleActivePluginRegistryMock(...args), })); let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider; @@ -37,6 +41,8 @@ describe("speech provider registry", () => { resetPluginRuntimeStateForTest(); loadOpenClawPluginsMock.mockReset(); loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry()); + getCompatibleActivePluginRegistryMock.mockReset(); + getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); ({ getSpeechProvider, listSpeechProviders, @@ -60,7 +66,16 @@ describe("speech provider registry", () => { }, ], }); - + getCompatibleActivePluginRegistryMock.mockReturnValue({ + ...createEmptyPluginRegistry(), + speechProviders: [ + { + pluginId: "test-demo-speech", + source: "test", + provider: createSpeechProvider("demo-speech"), + }, + ], + }); const providers = listSpeechProviders(); expect(providers.map((provider) => provider.id)).toEqual(["demo-speech"]); @@ -112,6 +127,16 @@ describe("speech provider registry", () => { }, ], }); + getCompatibleActivePluginRegistryMock.mockReturnValue({ + ...createEmptyPluginRegistry(), + speechProviders: [ + { + pluginId: "test-microsoft", + source: "test", + provider: createSpeechProvider("microsoft", ["edge"]), + }, + ], + }); expect(normalizeSpeechProviderId("edge")).toBe("edge"); expect(canonicalizeSpeechProviderId("edge")).toBe("microsoft");