mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
perf: scope reply runtime plugin startup
This commit is contained in:
@@ -160,6 +160,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Agents/tools: skip unavailable media generation and PDF tool factories from the live reply path when Gateway metadata and the active auth store prove no configured provider can back them, while keeping explicit config and auth-backed providers on the normal factory path. Thanks @shakkernerd.
|
||||
- Agents/runtime: reuse the Gateway metadata startup plan when ensuring reply runtime plugins are loaded, so live agent turns do not broad-load plugin runtimes after the Gateway already scoped startup activation. Thanks @shakkernerd.
|
||||
- Agents/tools: route media and generation capability lookups through the Gateway plugin metadata snapshot during reply tool registration, avoiding repeated manifest registry reloads on the live reply path. Thanks @shakkernerd.
|
||||
- Agents/tools: reuse the auth profile store already loaded for the active run when deciding media and generation tool availability, avoiding repeated provider-auth runtime discovery during reply startup. Thanks @shakkernerd.
|
||||
- Agents/tools: keep image, video, and music generation tool registration on manifest/auth control-plane checks instead of loading runtime provider registries during reply startup, reducing live-path tool-prep blocking while leaving provider runtime resolution for execution and list actions. Thanks @shakkernerd.
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
getCurrentPluginMetadataSnapshot: vi.fn(),
|
||||
resolveRuntimePluginRegistry: vi.fn(),
|
||||
getActivePluginRegistry: vi.fn(),
|
||||
getActivePluginRegistryWorkspaceDir: vi.fn(),
|
||||
getActivePluginRuntimeSubagentMode: vi.fn<() => "default" | "explicit" | "gateway-bindable">(
|
||||
() => "default",
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: hoisted.getCurrentPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
resolveRuntimePluginRegistry: hoisted.resolveRuntimePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: hoisted.getActivePluginRegistry,
|
||||
getActivePluginRegistryWorkspaceDir: hoisted.getActivePluginRegistryWorkspaceDir,
|
||||
getActivePluginRuntimeSubagentMode: hoisted.getActivePluginRuntimeSubagentMode,
|
||||
}));
|
||||
|
||||
@@ -19,8 +28,14 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
let ensureRuntimePluginsLoaded: typeof import("./runtime-plugins.js").ensureRuntimePluginsLoaded;
|
||||
|
||||
beforeEach(async () => {
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReset();
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined);
|
||||
hoisted.resolveRuntimePluginRegistry.mockReset();
|
||||
hoisted.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
hoisted.getActivePluginRegistry.mockReset();
|
||||
hoisted.getActivePluginRegistry.mockReturnValue(null);
|
||||
hoisted.getActivePluginRegistryWorkspaceDir.mockReset();
|
||||
hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue(undefined);
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReset();
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("default");
|
||||
vi.resetModules();
|
||||
@@ -55,6 +70,76 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("scopes runtime plugin loading to the current gateway startup plan", async () => {
|
||||
const config = {} as never;
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
|
||||
startup: {
|
||||
pluginIds: ["telegram", "memory-core"],
|
||||
},
|
||||
});
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
onlyPluginIds: ["telegram", "memory-core"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses an active gateway registry that already covers the startup plan", async () => {
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
|
||||
startup: {
|
||||
pluginIds: ["telegram"],
|
||||
},
|
||||
});
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("gateway-bindable");
|
||||
hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue("/tmp/workspace");
|
||||
hoisted.getActivePluginRegistry.mockReturnValue({
|
||||
plugins: [{ id: "telegram", status: "loaded" }],
|
||||
});
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reuse an active gateway registry for another workspace", async () => {
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
|
||||
startup: {
|
||||
pluginIds: ["telegram"],
|
||||
},
|
||||
});
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("gateway-bindable");
|
||||
hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue("/tmp/other-workspace");
|
||||
hoisted.getActivePluginRegistry.mockReturnValue({
|
||||
plugins: [{ id: "telegram", status: "loaded" }],
|
||||
});
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not enable gateway subagent binding for normal runtime loads", async () => {
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
|
||||
@@ -1,8 +1,63 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { resolveRuntimePluginRegistry } from "../plugins/loader.js";
|
||||
import { getActivePluginRuntimeSubagentMode } from "../plugins/runtime.js";
|
||||
import {
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryWorkspaceDir,
|
||||
getActivePluginRuntimeSubagentMode,
|
||||
} from "../plugins/runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
type StartupScopedPluginSnapshot = NonNullable<
|
||||
ReturnType<typeof getCurrentPluginMetadataSnapshot>
|
||||
> & {
|
||||
startup?: {
|
||||
pluginIds?: readonly unknown[];
|
||||
};
|
||||
};
|
||||
|
||||
function resolveStartupPluginIdsFromCurrentSnapshot(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): string[] | undefined {
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}) as StartupScopedPluginSnapshot | undefined;
|
||||
const pluginIds = snapshot?.startup?.pluginIds;
|
||||
if (!Array.isArray(pluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return pluginIds.filter((pluginId): pluginId is string => typeof pluginId === "string");
|
||||
}
|
||||
|
||||
function activeRegistryCoversStartupScope(params: {
|
||||
pluginIds: readonly string[];
|
||||
workspaceDir?: string;
|
||||
allowGatewaySubagentBinding: boolean;
|
||||
}): boolean {
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
if (!activeRegistry) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.allowGatewaySubagentBinding &&
|
||||
getActivePluginRuntimeSubagentMode() !== "gateway-bindable"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activeWorkspaceDir = getActivePluginRegistryWorkspaceDir();
|
||||
if (
|
||||
activeWorkspaceDir !== undefined &&
|
||||
params.workspaceDir !== undefined &&
|
||||
activeWorkspaceDir !== params.workspaceDir
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activePluginIds = new Set(activeRegistry.plugins.map((plugin) => plugin.id));
|
||||
return params.pluginIds.every((pluginId) => activePluginIds.has(pluginId));
|
||||
}
|
||||
|
||||
export function ensureRuntimePluginsLoaded(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string | null;
|
||||
@@ -15,9 +70,24 @@ export function ensureRuntimePluginsLoaded(params: {
|
||||
const allowGatewaySubagentBinding =
|
||||
params.allowGatewaySubagentBinding === true ||
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable";
|
||||
const startupPluginIds = resolveStartupPluginIdsFromCurrentSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
});
|
||||
if (
|
||||
startupPluginIds &&
|
||||
activeRegistryCoversStartupScope({
|
||||
pluginIds: startupPluginIds,
|
||||
workspaceDir,
|
||||
allowGatewaySubagentBinding,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const loadOptions = {
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
...(startupPluginIds ? { onlyPluginIds: startupPluginIds } : {}),
|
||||
runtimeOptions: allowGatewaySubagentBinding
|
||||
? {
|
||||
allowGatewaySubagentBinding: true,
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||
|
||||
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
|
||||
let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;
|
||||
let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;
|
||||
let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;
|
||||
let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
|
||||
|
||||
@@ -226,7 +227,8 @@ function expectConflictingCoreNameResolution(params: {
|
||||
describe("resolvePluginTools optional tools", () => {
|
||||
beforeAll(async () => {
|
||||
({ buildPluginToolMetadataKey, resolvePluginTools } = await import("./tools.js"));
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
|
||||
({ pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry } =
|
||||
await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -544,7 +546,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes gateway-bindable tool loads through scoped runtime compatibility", () => {
|
||||
it("reuses the gateway-bindable registry when it covers the tool runtime scope", () => {
|
||||
const activeRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
@@ -557,14 +559,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["optional-demo"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -597,6 +592,55 @@ describe("resolvePluginTools optional tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reuses the pinned gateway channel registry after provider runtime loads replace active registry", () => {
|
||||
const gatewayRegistry = createOptionalDemoActiveRegistry();
|
||||
pinActivePluginChannelRegistry(gatewayRegistry as never);
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
tools: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"provider-runtime",
|
||||
"default",
|
||||
);
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
toolAllowlist: ["optional_tool"],
|
||||
allowGatewaySubagentBinding: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses the pinned gateway channel registry even when the caller omits gateway binding", () => {
|
||||
const gatewayRegistry = createOptionalDemoActiveRegistry();
|
||||
pinActivePluginChannelRegistry(gatewayRegistry as never);
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
tools: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"provider-runtime",
|
||||
"default",
|
||||
);
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
toolAllowlist: ["optional_tool"],
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads plugin tools when gateway-bindable tool loads have no active registry", () => {
|
||||
setOptionalDemoRegistry();
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.
|
||||
import { listEnabledInstalledPluginRecords } from "./installed-plugin-index.js";
|
||||
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry-snapshot.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import {
|
||||
getActivePluginChannelRegistry,
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
getActivePluginRuntimeSubagentMode,
|
||||
} from "./runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
@@ -194,18 +199,25 @@ function describeMalformedPluginTool(tool: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function addLoadedPluginIdsFromRegistry(
|
||||
registry: ReturnType<typeof getActivePluginRegistry>,
|
||||
pluginIds: Set<string>,
|
||||
): void {
|
||||
for (const plugin of registry?.plugins ?? []) {
|
||||
if (plugin.status === undefined || plugin.status === "loaded") {
|
||||
pluginIds.add(plugin.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePluginToolRuntimePluginIds(params: {
|
||||
config: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] | undefined {
|
||||
const pluginIds = new Set<string>();
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
for (const plugin of activeRegistry?.plugins ?? []) {
|
||||
if (plugin.status === undefined || plugin.status === "loaded") {
|
||||
pluginIds.add(plugin.id);
|
||||
}
|
||||
}
|
||||
addLoadedPluginIdsFromRegistry(getActivePluginChannelRegistry(), pluginIds);
|
||||
addLoadedPluginIdsFromRegistry(getActivePluginRegistry(), pluginIds);
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -219,6 +231,37 @@ function resolvePluginToolRuntimePluginIds(params: {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function registryContainsPluginIds(
|
||||
registry: ReturnType<typeof getActivePluginRegistry>,
|
||||
pluginIds?: readonly string[],
|
||||
): boolean {
|
||||
if (!registry || pluginIds === undefined) {
|
||||
return false;
|
||||
}
|
||||
const loadedPluginIds = new Set<string>();
|
||||
addLoadedPluginIdsFromRegistry(registry, loadedPluginIds);
|
||||
return pluginIds.every((pluginId) => loadedPluginIds.has(pluginId));
|
||||
}
|
||||
|
||||
function resolvePluginToolRegistry(params: {
|
||||
loadOptions: PluginLoadOptions;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}) {
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
const channelRegistry = getActivePluginChannelRegistry();
|
||||
const activeRegistryIsGatewayBindable =
|
||||
getActivePluginRegistryKey() && getActivePluginRuntimeSubagentMode() === "gateway-bindable";
|
||||
const hasPinnedGatewayRegistry = Boolean(channelRegistry && channelRegistry !== activeRegistry);
|
||||
if (
|
||||
channelRegistry &&
|
||||
(activeRegistryIsGatewayBindable || hasPinnedGatewayRegistry) &&
|
||||
registryContainsPluginIds(channelRegistry, params.onlyPluginIds)
|
||||
) {
|
||||
return channelRegistry;
|
||||
}
|
||||
return resolveRuntimePluginRegistry(params.loadOptions);
|
||||
}
|
||||
|
||||
export function resolvePluginTools(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
existingToolNames?: Set<string>;
|
||||
@@ -255,7 +298,10 @@ export function resolvePluginTools(params: {
|
||||
...(onlyPluginIds !== undefined ? { onlyPluginIds } : {}),
|
||||
runtimeOptions,
|
||||
});
|
||||
const registry = resolveRuntimePluginRegistry(loadOptions);
|
||||
const registry = resolvePluginToolRegistry({
|
||||
loadOptions,
|
||||
onlyPluginIds,
|
||||
});
|
||||
if (!registry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user