Files
openclaw/src/plugins/tool-descriptor-cache.ts
2026-05-10 15:21:43 +01:00

189 lines
6.0 KiB
TypeScript

import fs from "node:fs";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import type { JsonObject, ToolDescriptor } from "../tools/types.js";
import type { PluginLoadOptions } from "./loader.js";
import type { OpenClawPluginToolContext } from "./types.js";
const PLUGIN_TOOL_DESCRIPTOR_CACHE_VERSION = 1;
const PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT = 256;
export type CachedPluginToolDescriptor = {
descriptor: ToolDescriptor;
displaySummary?: string;
ownerOnly?: boolean;
optional: boolean;
};
const descriptorCache = new Map<string, CachedPluginToolDescriptor[]>();
let descriptorCacheObjectIds = new WeakMap<object, number>();
let nextDescriptorCacheObjectId = 1;
export type PluginToolDescriptorConfigCacheKeyMemo = WeakMap<object, string | number | null>;
export function createPluginToolDescriptorConfigCacheKeyMemo(): PluginToolDescriptorConfigCacheKeyMemo {
return new WeakMap();
}
export function resetPluginToolDescriptorCache(): void {
descriptorCache.clear();
descriptorCacheObjectIds = new WeakMap();
nextDescriptorCacheObjectId = 1;
}
function sourceFingerprint(source: string): string {
try {
const stat = fs.statSync(source);
return `${stat.size}:${Math.round(stat.mtimeMs)}`;
} catch {
return "missing";
}
}
function getDescriptorCacheObjectId(value: object | null | undefined): number | null {
if (!value) {
return null;
}
const existing = descriptorCacheObjectIds.get(value);
if (existing !== undefined) {
return existing;
}
const next = nextDescriptorCacheObjectId++;
descriptorCacheObjectIds.set(value, next);
return next;
}
function stripDescriptorVolatileConfigFields(
value: NonNullable<PluginLoadOptions["config"]>,
): NonNullable<PluginLoadOptions["config"]> {
if (typeof value !== "object") {
return value;
}
if (!("meta" in value) && !("wizard" in value)) {
return value;
}
const { meta: _meta, wizard: _wizard, ...stableConfig } = value as Record<string, unknown>;
return stableConfig as NonNullable<PluginLoadOptions["config"]>;
}
function getDescriptorConfigCacheKey(
value: PluginLoadOptions["config"] | null | undefined,
memo?: PluginToolDescriptorConfigCacheKeyMemo,
): string | number | null {
if (!value) {
return null;
}
const cached = memo?.get(value);
if (cached !== undefined) {
return cached;
}
let resolved: string | number | null;
try {
resolved = resolveRuntimeConfigCacheKey(stripDescriptorVolatileConfigFields(value));
} catch {
resolved = getDescriptorCacheObjectId(value);
}
memo?.set(value, resolved);
return resolved;
}
function buildDescriptorContextCacheKey(params: {
ctx: OpenClawPluginToolContext;
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
}): string {
const { ctx } = params;
return JSON.stringify({
config: getDescriptorConfigCacheKey(ctx.config, params.configCacheKeyMemo),
runtimeConfig: getDescriptorConfigCacheKey(ctx.runtimeConfig, params.configCacheKeyMemo),
currentRuntimeConfig: getDescriptorConfigCacheKey(
params.currentRuntimeConfig,
params.configCacheKeyMemo,
),
fsPolicy: ctx.fsPolicy ?? null,
workspaceDir: ctx.workspaceDir ?? null,
agentDir: ctx.agentDir ?? null,
agentId: ctx.agentId ?? null,
activeModel: ctx.activeModel ?? 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 buildPluginToolDescriptorCacheKey(params: {
pluginId: string;
source: string;
rootDir?: string;
contractToolNames: readonly string[];
ctx: OpenClawPluginToolContext;
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
}): string {
return JSON.stringify({
version: PLUGIN_TOOL_DESCRIPTOR_CACHE_VERSION,
pluginId: params.pluginId,
source: params.source,
rootDir: params.rootDir ?? null,
sourceFingerprint: sourceFingerprint(params.source),
contractToolNames: [...params.contractToolNames].toSorted(),
context: buildDescriptorContextCacheKey({
ctx: params.ctx,
currentRuntimeConfig: params.currentRuntimeConfig,
configCacheKeyMemo: params.configCacheKeyMemo,
}),
});
}
function asJsonObject(value: unknown): JsonObject {
return value as JsonObject;
}
export function capturePluginToolDescriptor(params: {
pluginId: string;
tool: AnyAgentTool;
optional: boolean;
}): CachedPluginToolDescriptor {
const label = (params.tool as { label?: unknown }).label;
const title = typeof label === "string" && label.trim() ? label.trim() : undefined;
return {
...(params.tool.displaySummary ? { displaySummary: params.tool.displaySummary } : {}),
...(params.tool.ownerOnly === true ? { ownerOnly: true } : {}),
optional: params.optional,
descriptor: {
name: params.tool.name,
...(title ? { title } : {}),
description: params.tool.description,
inputSchema: asJsonObject(params.tool.parameters),
owner: { kind: "plugin", pluginId: params.pluginId },
executor: { kind: "plugin", pluginId: params.pluginId, toolName: params.tool.name },
},
};
}
export function readCachedPluginToolDescriptors(
cacheKey: string,
): readonly CachedPluginToolDescriptor[] | undefined {
return descriptorCache.get(cacheKey);
}
export function writeCachedPluginToolDescriptors(params: {
cacheKey: string;
descriptors: readonly CachedPluginToolDescriptor[];
}): void {
if (
!descriptorCache.has(params.cacheKey) &&
descriptorCache.size >= PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT
) {
const oldestKey = descriptorCache.keys().next().value;
if (oldestKey !== undefined) {
descriptorCache.delete(oldestKey);
}
}
descriptorCache.set(params.cacheKey, [...params.descriptors]);
}