diff --git a/src/agents/openclaw-tools.media-factory-plan.test.ts b/src/agents/openclaw-tools.media-factory-plan.test.ts index 171e986efbf..ba250c286ef 100644 --- a/src/agents/openclaw-tools.media-factory-plan.test.ts +++ b/src/agents/openclaw-tools.media-factory-plan.test.ts @@ -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), diff --git a/src/plugins/tool-factory-cache.ts b/src/plugins/tool-factory-cache.ts new file mode 100644 index 00000000000..e268cbea057 --- /dev/null +++ b/src/plugins/tool-factory-cache.ts @@ -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 +>(); +let pluginToolFactoryCacheObjectIds = new WeakMap(); +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); +} diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index ddfdf18fb4c..cd0d3bef244 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -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 ->(); -let pluginToolFactoryCacheObjectIds = new WeakMap(); -let nextPluginToolFactoryCacheObjectId = 1; const pluginToolMeta = new WeakMap(); -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); }