refactor: extract plugin tool factory cache

This commit is contained in:
Peter Steinberger
2026-05-02 12:41:50 +01:00
parent e62608beaa
commit d90a08a447
3 changed files with 145 additions and 119 deletions

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js";
import {
@@ -10,6 +10,7 @@ import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plug
import type { InstalledPluginIndexRecord } from "../plugins/installed-plugin-index.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import { clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
import type { AuthProfileStore } from "./auth-profiles/types.js";
import { __testing, createOpenClawTools } from "./openclaw-tools.js";
@@ -141,8 +142,13 @@ function installSnapshot(
}
describe("optional media tool factory planning", () => {
beforeEach(() => {
clearSecretsRuntimeSnapshot();
});
afterEach(() => {
clearCurrentPluginMetadataSnapshot();
clearSecretsRuntimeSnapshot();
setBundledPluginsDirOverrideForTest(undefined);
vi.unstubAllEnvs();
});
@@ -500,6 +506,7 @@ describe("optional media tool factory planning", () => {
it("does not count unresolved SecretRef config signals as configured", () => {
vi.stubEnv("COMFY_TEST_API_KEY", "");
const workspaceDir = process.cwd();
const config: OpenClawConfig = {
plugins: {
entries: {
@@ -525,29 +532,35 @@ describe("optional media tool factory planning", () => {
required: ["promptNodeId", "apiKey"],
},
];
installSnapshot(config, [
createPlugin({
id: "comfy",
contracts: {
imageGenerationProviders: ["comfy"],
videoGenerationProviders: ["comfy"],
musicGenerationProviders: ["comfy"],
},
imageGenerationProviderMetadata: {
comfy: { configSignals },
},
videoGenerationProviderMetadata: {
comfy: { configSignals },
},
musicGenerationProviderMetadata: {
comfy: { configSignals },
},
}),
]);
installSnapshot(
config,
[
createPlugin({
id: "comfy",
contracts: {
imageGenerationProviders: ["comfy"],
videoGenerationProviders: ["comfy"],
musicGenerationProviders: ["comfy"],
},
imageGenerationProviderMetadata: {
comfy: { configSignals },
},
videoGenerationProviderMetadata: {
comfy: { configSignals },
},
musicGenerationProviderMetadata: {
comfy: { configSignals },
},
}),
],
undefined,
workspaceDir,
);
expect(
__testing.resolveOptionalMediaToolFactoryPlan({
config,
workspaceDir,
authStore: createAuthStore(),
}),
).toEqual({
@@ -559,6 +572,7 @@ describe("optional media tool factory planning", () => {
expect(
createOpenClawTools({
config,
workspaceDir,
authProfileStore: createAuthStore(),
pluginToolAllowlist: ["image_generate", "video_generate", "music_generate"],
}).map((tool) => tool.name),

View File

@@ -0,0 +1,102 @@
import type { AnyAgentTool } from "../agents/tools/common.js";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import type { PluginLoadOptions } from "./loader.js";
import type { OpenClawPluginToolContext, OpenClawPluginToolFactory } from "./types.js";
const PLUGIN_TOOL_FACTORY_CACHE_LIMIT_PER_FACTORY = 64;
export type PluginToolFactoryResult = AnyAgentTool | AnyAgentTool[] | null | undefined;
let pluginToolFactoryCache = new WeakMap<
OpenClawPluginToolFactory,
Map<string, PluginToolFactoryResult>
>();
let pluginToolFactoryCacheObjectIds = new WeakMap<object, number>();
let nextPluginToolFactoryCacheObjectId = 1;
export function resetPluginToolFactoryCache(): void {
pluginToolFactoryCache = new WeakMap();
pluginToolFactoryCacheObjectIds = new WeakMap();
nextPluginToolFactoryCacheObjectId = 1;
}
function getPluginToolFactoryCacheObjectId(value: object | null | undefined): number | null {
if (!value) {
return null;
}
const existing = pluginToolFactoryCacheObjectIds.get(value);
if (existing !== undefined) {
return existing;
}
const next = nextPluginToolFactoryCacheObjectId++;
pluginToolFactoryCacheObjectIds.set(value, next);
return next;
}
function getPluginToolFactoryConfigCacheKey(
value: PluginLoadOptions["config"] | null | undefined,
): string | number | null {
if (!value) {
return null;
}
try {
return resolveRuntimeConfigCacheKey(value);
} catch {
return getPluginToolFactoryCacheObjectId(value);
}
}
export function buildPluginToolFactoryCacheKey(params: {
ctx: OpenClawPluginToolContext;
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
}): string {
const { ctx } = params;
return JSON.stringify({
config: getPluginToolFactoryConfigCacheKey(ctx.config),
runtimeConfig: getPluginToolFactoryConfigCacheKey(ctx.runtimeConfig),
currentRuntimeConfig: getPluginToolFactoryConfigCacheKey(params.currentRuntimeConfig),
fsPolicy: ctx.fsPolicy ?? null,
workspaceDir: ctx.workspaceDir ?? null,
agentDir: ctx.agentDir ?? null,
agentId: ctx.agentId ?? null,
sessionKey: ctx.sessionKey ?? null,
sessionId: ctx.sessionId ?? null,
browser: ctx.browser ?? null,
messageChannel: ctx.messageChannel ?? null,
agentAccountId: ctx.agentAccountId ?? null,
deliveryContext: ctx.deliveryContext ?? null,
requesterSenderId: ctx.requesterSenderId ?? null,
senderIsOwner: ctx.senderIsOwner ?? null,
sandboxed: ctx.sandboxed ?? null,
});
}
export function readCachedPluginToolFactoryResult(params: {
factory: OpenClawPluginToolFactory;
cacheKey: string;
}): { hit: boolean; result: PluginToolFactoryResult } {
const cache = pluginToolFactoryCache.get(params.factory);
if (!cache || !cache.has(params.cacheKey)) {
return { hit: false, result: undefined };
}
return { hit: true, result: cache.get(params.cacheKey) };
}
export function writeCachedPluginToolFactoryResult(params: {
factory: OpenClawPluginToolFactory;
cacheKey: string;
result: PluginToolFactoryResult;
}): void {
let cache = pluginToolFactoryCache.get(params.factory);
if (!cache) {
cache = new Map();
pluginToolFactoryCache.set(params.factory, cache);
}
if (!cache.has(params.cacheKey) && cache.size >= PLUGIN_TOOL_FACTORY_CACHE_LIMIT_PER_FACTORY) {
const oldestKey = cache.keys().next().value;
if (oldestKey !== undefined) {
cache.delete(oldestKey);
}
}
cache.set(params.cacheKey, params.result);
}

View File

@@ -1,6 +1,5 @@
import { normalizeToolName } from "../agents/tool-policy.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
@@ -20,7 +19,15 @@ import {
resolvePluginRuntimeLoadContext,
} from "./runtime/load-context.js";
import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
import type { OpenClawPluginToolContext, OpenClawPluginToolFactory } from "./types.js";
import {
buildPluginToolFactoryCacheKey,
readCachedPluginToolFactoryResult,
type PluginToolFactoryResult,
writeCachedPluginToolFactoryResult,
} from "./tool-factory-cache.js";
import type { OpenClawPluginToolContext } from "./types.js";
export { resetPluginToolFactoryCache } from "./tool-factory-cache.js";
export type PluginToolMeta = {
pluginId: string;
@@ -43,106 +50,9 @@ const log = createSubsystemLogger("plugins/tools");
const PLUGIN_TOOL_FACTORY_WARN_TOTAL_MS = 5_000;
const PLUGIN_TOOL_FACTORY_WARN_FACTORY_MS = 1_000;
const PLUGIN_TOOL_FACTORY_SUMMARY_LIMIT = 20;
const PLUGIN_TOOL_FACTORY_CACHE_LIMIT_PER_FACTORY = 64;
type PluginToolFactoryResult = AnyAgentTool | AnyAgentTool[] | null | undefined;
let pluginToolFactoryCache = new WeakMap<
OpenClawPluginToolFactory,
Map<string, PluginToolFactoryResult>
>();
let pluginToolFactoryCacheObjectIds = new WeakMap<object, number>();
let nextPluginToolFactoryCacheObjectId = 1;
const pluginToolMeta = new WeakMap<AnyAgentTool, PluginToolMeta>();
export function resetPluginToolFactoryCache(): void {
pluginToolFactoryCache = new WeakMap();
pluginToolFactoryCacheObjectIds = new WeakMap();
nextPluginToolFactoryCacheObjectId = 1;
}
function getPluginToolFactoryCacheObjectId(value: object | null | undefined): number | null {
if (!value) {
return null;
}
const existing = pluginToolFactoryCacheObjectIds.get(value);
if (existing !== undefined) {
return existing;
}
const next = nextPluginToolFactoryCacheObjectId++;
pluginToolFactoryCacheObjectIds.set(value, next);
return next;
}
function getPluginToolFactoryConfigCacheKey(
value: PluginLoadOptions["config"] | null | undefined,
): string | number | null {
if (!value) {
return null;
}
try {
return resolveRuntimeConfigCacheKey(value);
} catch {
return getPluginToolFactoryCacheObjectId(value);
}
}
function buildPluginToolFactoryCacheKey(params: {
ctx: OpenClawPluginToolContext;
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
}): string {
const { ctx } = params;
return JSON.stringify({
config: getPluginToolFactoryConfigCacheKey(ctx.config),
runtimeConfig: getPluginToolFactoryConfigCacheKey(ctx.runtimeConfig),
currentRuntimeConfig: getPluginToolFactoryConfigCacheKey(params.currentRuntimeConfig),
fsPolicy: ctx.fsPolicy ?? null,
workspaceDir: ctx.workspaceDir ?? null,
agentDir: ctx.agentDir ?? null,
agentId: ctx.agentId ?? null,
sessionKey: ctx.sessionKey ?? null,
sessionId: ctx.sessionId ?? null,
browser: ctx.browser ?? null,
messageChannel: ctx.messageChannel ?? null,
agentAccountId: ctx.agentAccountId ?? null,
deliveryContext: ctx.deliveryContext ?? null,
requesterSenderId: ctx.requesterSenderId ?? null,
senderIsOwner: ctx.senderIsOwner ?? null,
sandboxed: ctx.sandboxed ?? null,
});
}
function readCachedPluginToolFactoryResult(params: {
factory: OpenClawPluginToolFactory;
cacheKey: string;
}): { hit: boolean; result: PluginToolFactoryResult } {
const cache = pluginToolFactoryCache.get(params.factory);
if (!cache || !cache.has(params.cacheKey)) {
return { hit: false, result: undefined };
}
return { hit: true, result: cache.get(params.cacheKey) };
}
function writeCachedPluginToolFactoryResult(params: {
factory: OpenClawPluginToolFactory;
cacheKey: string;
result: PluginToolFactoryResult;
}): void {
let cache = pluginToolFactoryCache.get(params.factory);
if (!cache) {
cache = new Map();
pluginToolFactoryCache.set(params.factory, cache);
}
if (!cache.has(params.cacheKey) && cache.size >= PLUGIN_TOOL_FACTORY_CACHE_LIMIT_PER_FACTORY) {
const oldestKey = cache.keys().next().value;
if (oldestKey !== undefined) {
cache.delete(oldestKey);
}
}
cache.set(params.cacheKey, params.result);
}
export function setPluginToolMeta(tool: AnyAgentTool, meta: PluginToolMeta): void {
pluginToolMeta.set(tool, meta);
}