feat(dynamic-tools-cache): 4 cache tools + global __padded API
Implements the openclaw side of Plexum decision #37: agent-driven per-session whitelist of which tools the model sees. Cross-runtime aligned with Plexum's host implementation (same tool names, same input schemas, takes-effect-next-turn). New surface: - tools/dynamic-tools-cache.ts: Catalog (in-memory map), Cache file (atomic tmp+rename at <openclaw>/agents/<id>/sessions/<sid>/dynamic-tools-cache.json), 4 tool factories (list/search/cache/evict), allowTool gate, installGlobalApi for globalThis.__padded with buffer-drain - index.ts: install __padded early; register the 4 cache tools as factories wrapping ctx into execute - openclaw.plugin.json contracts.tools += 4 names Other Hangman-Lab plugins (Fabric / HF / Dialectic) opt in to the gate by wrapping their registerTool calls (separate commits in those repos). Built-in openclaw tools and non-opt-in plugins remain always-on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,13 @@ import {
|
|||||||
queueDynamicTrim,
|
queueDynamicTrim,
|
||||||
drainTrimQueueForSession,
|
drainTrimQueueForSession,
|
||||||
} from './tools/session-rewrite.js';
|
} from './tools/session-rewrite.js';
|
||||||
|
import {
|
||||||
|
installGlobalApi as installPaddedGlobalApi,
|
||||||
|
createListToolsTool,
|
||||||
|
createSearchToolsTool,
|
||||||
|
createCacheToolsTool,
|
||||||
|
createEvictToolsTool,
|
||||||
|
} from './tools/dynamic-tools-cache.js';
|
||||||
import {
|
import {
|
||||||
safeRestart,
|
safeRestart,
|
||||||
createSafeRestartTool,
|
createSafeRestartTool,
|
||||||
@@ -51,6 +58,12 @@ function register(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
logger.info('PaddedCell plugin initializing...');
|
logger.info('PaddedCell plugin initializing...');
|
||||||
|
|
||||||
|
// Install globalThis.__padded EARLY so other plugins loaded after
|
||||||
|
// PaddedCell can register catalog entries / query allowTool. Plugins
|
||||||
|
// loaded BEFORE PaddedCell can pre-fill _pendingCatalog on a stub;
|
||||||
|
// installGlobalApi drains it.
|
||||||
|
installPaddedGlobalApi();
|
||||||
|
|
||||||
const pluginConfig = getPluginConfig(api);
|
const pluginConfig = getPluginConfig(api);
|
||||||
const openclawPath = resolveOpenclawPath(pluginConfig as { openclawProfilePath?: string });
|
const openclawPath = resolveOpenclawPath(pluginConfig as { openclawProfilePath?: string });
|
||||||
const proxyAllowlist = resolveProxyAllowlist(pluginConfig as { proxyAllowlist?: unknown; 'proxy-allowlist'?: unknown });
|
const proxyAllowlist = resolveProxyAllowlist(pluginConfig as { proxyAllowlist?: unknown; 'proxy-allowlist'?: unknown });
|
||||||
@@ -241,6 +254,27 @@ function register(api: OpenClawPluginApi): void {
|
|||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dynamic-{list,search,cache,evict}-tools — per-session tools-cache
|
||||||
|
// family (cross-runtime alignment with Plexum decision #37). All four
|
||||||
|
// are essentials (always exposed). The cache they manage gates other
|
||||||
|
// opt-in plugins via globalThis.__padded.allowTool — see
|
||||||
|
// tools/dynamic-tools-cache.ts and plans/OPENCLAW_TOOLS_FILTER_HOOK.md.
|
||||||
|
const cacheToolFactories = [
|
||||||
|
createListToolsTool,
|
||||||
|
createSearchToolsTool,
|
||||||
|
createCacheToolsTool,
|
||||||
|
createEvictToolsTool,
|
||||||
|
];
|
||||||
|
for (const make of cacheToolFactories) {
|
||||||
|
api.registerTool((ctx) => {
|
||||||
|
const tool = make();
|
||||||
|
const inner = tool.execute;
|
||||||
|
tool.execute = async (callId: string, params: any) =>
|
||||||
|
inner(callId, params, { agentId: ctx.agentId, sessionId: ctx.sessionId });
|
||||||
|
return tool;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register /ego-mgr slash command if the host exposes the (non-standard) hook.
|
// Register /ego-mgr slash command if the host exposes the (non-standard) hook.
|
||||||
// This API is not part of the current OpenClawPluginApi surface; the guard
|
// This API is not part of the current OpenClawPluginApi surface; the guard
|
||||||
// makes the plugin a no-op for slash commands when the host doesn't support
|
// makes the plugin a no-op for slash commands when the host doesn't support
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
"pcexec",
|
"pcexec",
|
||||||
"proxy-pcexec",
|
"proxy-pcexec",
|
||||||
"safe_restart",
|
"safe_restart",
|
||||||
"dynamic-trim"
|
"dynamic-trim",
|
||||||
|
"dynamic-list-tools",
|
||||||
|
"dynamic-search-tools",
|
||||||
|
"dynamic-cache-tools",
|
||||||
|
"dynamic-evict-tools"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
|
|||||||
403
plugin/tools/dynamic-tools-cache.ts
Normal file
403
plugin/tools/dynamic-tools-cache.ts
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
// `dynamic-list-tools` / `dynamic-search-tools` / `dynamic-cache-tools` /
|
||||||
|
// `dynamic-evict-tools` — agent-driven per-session tool visibility gate.
|
||||||
|
//
|
||||||
|
// openclaw plugin SDK has no `before_outgoing_tools` hook (see PaddedCell
|
||||||
|
// plans/OPENCLAW_TOOLS_FILTER_HOOK.md for the gap analysis), so this
|
||||||
|
// implementation relies on a per-plugin opt-in pattern via the global
|
||||||
|
// __padded API:
|
||||||
|
//
|
||||||
|
// - PaddedCell exposes:
|
||||||
|
// globalThis.__padded.registerCatalogEntry(name, description)
|
||||||
|
// globalThis.__padded.allowTool(name, ctx) => boolean
|
||||||
|
// - Other Hangman-Lab plugins wrap their tool factories:
|
||||||
|
// api.registerTool((ctx) => {
|
||||||
|
// if (!globalThis.__padded?.allowTool?.(NAME, ctx)) return null;
|
||||||
|
// return realTool;
|
||||||
|
// });
|
||||||
|
// and at init:
|
||||||
|
// globalThis.__padded?.registerCatalogEntry?.(NAME, DESC);
|
||||||
|
//
|
||||||
|
// Built-in openclaw tools (read/edit/grep/bash) and plugins that don't
|
||||||
|
// opt in to the gate are always-on. The 4 tools below are themselves
|
||||||
|
// always-on (essentials, hardcoded). Per-session cache state lives at
|
||||||
|
// <openclaw>/agents/<agentId>/sessions/<sessionId>/dynamic-tools-cache.json.
|
||||||
|
//
|
||||||
|
// Cross-runtime aligned with Plexum's decision #37 family — same tool
|
||||||
|
// names, same input schemas, same takes-effect-next-turn semantics.
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
/** Names of tools always exposed (the 4 cache tools themselves). */
|
||||||
|
export const ESSENTIAL_CACHE_TOOLS = new Set([
|
||||||
|
'dynamic-list-tools',
|
||||||
|
'dynamic-search-tools',
|
||||||
|
'dynamic-cache-tools',
|
||||||
|
'dynamic-evict-tools',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Catalog entry registered by an opt-in plugin. */
|
||||||
|
export interface CatalogEntry {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheFileShape {
|
||||||
|
version: number;
|
||||||
|
cached: Array<{ name: string; added_at_turn: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_FILE_NAME = 'dynamic-tools-cache.json';
|
||||||
|
|
||||||
|
function openclawRoot(): string {
|
||||||
|
const env = process.env.OPENCLAW_PATH;
|
||||||
|
if (env) return env;
|
||||||
|
const home = process.env.HOME || os.homedir();
|
||||||
|
return path.join(home, '.openclaw');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheFilePath(agentId: string | undefined, sessionId: string | undefined): string | null {
|
||||||
|
if (!agentId || !sessionId) return null;
|
||||||
|
return path.join(openclawRoot(), 'agents', agentId, 'sessions', sessionId, CACHE_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCache(filePath: string): CacheFileShape {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(filePath, 'utf8');
|
||||||
|
if (!raw) return { version: 1, cached: [] };
|
||||||
|
const parsed = JSON.parse(raw) as Partial<CacheFileShape>;
|
||||||
|
return { version: parsed.version ?? 1, cached: parsed.cached ?? [] };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
|
return { version: 1, cached: [] };
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCache(filePath: string, data: CacheFileShape): void {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
const tmp = `${filePath}.tmp`;
|
||||||
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
||||||
|
fs.renameSync(tmp, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** In-process catalog of opt-in plugin tools. */
|
||||||
|
class Catalog {
|
||||||
|
private entries = new Map<string, string>();
|
||||||
|
|
||||||
|
register(name: string, description: string): void {
|
||||||
|
if (!name) return;
|
||||||
|
this.entries.set(name, description ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
has(name: string): boolean {
|
||||||
|
return this.entries.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(): CatalogEntry[] {
|
||||||
|
return Array.from(this.entries, ([name, description]) => ({ name, description }));
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: string): CatalogEntry[] {
|
||||||
|
const q = query.toLowerCase().trim();
|
||||||
|
if (!q) return [];
|
||||||
|
return this.list().filter(
|
||||||
|
(e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalog = new Catalog();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drain any catalog entries that were queued before PaddedCell finished
|
||||||
|
* initializing. The buffer-drain pattern lets other plugins call
|
||||||
|
* `globalThis.__padded.registerCatalogEntry` at load time without
|
||||||
|
* worrying about load order; PaddedCell installs an early stub with
|
||||||
|
* `_pendingCatalog: []`, and on real init we slurp those into `catalog`.
|
||||||
|
*/
|
||||||
|
export function drainPendingCatalog(): number {
|
||||||
|
const stub = (globalThis as any).__padded;
|
||||||
|
if (!stub || !Array.isArray(stub._pendingCatalog)) return 0;
|
||||||
|
let n = 0;
|
||||||
|
for (const e of stub._pendingCatalog) {
|
||||||
|
if (e?.name) {
|
||||||
|
catalog.register(String(e.name), String(e.description ?? ''));
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stub._pendingCatalog = [];
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* allowTool — the gate every opt-in plugin's factory queries. Returns
|
||||||
|
* true iff: the name is an essential cache tool, OR it's in the
|
||||||
|
* per-session cache file. ctx must carry agentId + sessionId (factories
|
||||||
|
* receive them from openclaw).
|
||||||
|
*/
|
||||||
|
export function allowTool(name: string, ctx: { agentId?: string; sessionId?: string }): boolean {
|
||||||
|
if (ESSENTIAL_CACHE_TOOLS.has(name)) return true;
|
||||||
|
const fp = cacheFilePath(ctx.agentId, ctx.sessionId);
|
||||||
|
if (!fp) return true; // no session context => can't gate, default-allow
|
||||||
|
try {
|
||||||
|
const cache = readCache(fp);
|
||||||
|
return cache.cached.some((e) => e.name === name);
|
||||||
|
} catch {
|
||||||
|
return true; // any read error => default-allow (don't break agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Install the global API onto globalThis. Idempotent — drains a pre-existing stub. */
|
||||||
|
export function installGlobalApi(): void {
|
||||||
|
const existing = (globalThis as any).__padded;
|
||||||
|
const api = {
|
||||||
|
registerCatalogEntry(name: string, description: string): void {
|
||||||
|
catalog.register(name, description);
|
||||||
|
},
|
||||||
|
allowTool,
|
||||||
|
_pendingCatalog: [] as Array<{ name: string; description: string }>,
|
||||||
|
};
|
||||||
|
(globalThis as any).__padded = api;
|
||||||
|
if (existing?._pendingCatalog?.length) {
|
||||||
|
for (const e of existing._pendingCatalog) {
|
||||||
|
if (e?.name) catalog.register(String(e.name), String(e.description ?? ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test hook — wipe catalog state between tests. */
|
||||||
|
export function _resetCatalogForTest(): void {
|
||||||
|
(catalog as any).entries = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- tool factories -----
|
||||||
|
|
||||||
|
interface ToolCtx {
|
||||||
|
agentId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTurn(): number {
|
||||||
|
// openclaw plugin ctx doesn't carry turn number; record 0 as a
|
||||||
|
// placeholder. The field is informational (sorting / debug) — the
|
||||||
|
// gate semantics don't depend on it.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifySource(name: string, ctx: ToolCtx): 'essentials' | 'cached' | 'available' {
|
||||||
|
if (ESSENTIAL_CACHE_TOOLS.has(name)) return 'essentials';
|
||||||
|
const fp = cacheFilePath(ctx.agentId, ctx.sessionId);
|
||||||
|
if (fp) {
|
||||||
|
try {
|
||||||
|
const c = readCache(fp);
|
||||||
|
if (c.cached.some((e) => e.name === name)) return 'cached';
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'available';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCatalogText(
|
||||||
|
entries: CatalogEntry[],
|
||||||
|
ctx: ToolCtx,
|
||||||
|
header: string,
|
||||||
|
): string {
|
||||||
|
let ess = 0, cac = 0, avl = 0;
|
||||||
|
const tagged = entries.map((e) => {
|
||||||
|
const src = classifySource(e.name, ctx);
|
||||||
|
if (src === 'essentials') ess++;
|
||||||
|
else if (src === 'cached') cac++;
|
||||||
|
else avl++;
|
||||||
|
return { ...e, source: src };
|
||||||
|
});
|
||||||
|
const sourceRank = (s: string) => (s === 'essentials' ? 0 : s === 'cached' ? 1 : 2);
|
||||||
|
tagged.sort((a, b) => {
|
||||||
|
const ra = sourceRank(a.source), rb = sourceRank(b.source);
|
||||||
|
if (ra !== rb) return ra - rb;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(header);
|
||||||
|
lines.push(`total: ${tagged.length} (essentials: ${ess}, cached: ${cac}, available: ${avl})`);
|
||||||
|
lines.push('');
|
||||||
|
for (const t of tagged) {
|
||||||
|
lines.push(`[${t.source}] ${t.name}`);
|
||||||
|
lines.push(` ${t.description}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createListToolsTool(): any {
|
||||||
|
return {
|
||||||
|
name: 'dynamic-list-tools',
|
||||||
|
description:
|
||||||
|
'List the per-session tool catalog (essentials / cached / available). Pair with dynamic-cache-tools to enable specific tools for this session. Cache takes effect next turn.',
|
||||||
|
parameters: { type: 'object', properties: {}, required: [] },
|
||||||
|
async execute(_callId: string, _params: any, ctx?: ToolCtx) {
|
||||||
|
const c = ctx ?? {};
|
||||||
|
const entries = catalog.list();
|
||||||
|
// Also include essential cache tools themselves so the catalog is honest
|
||||||
|
for (const n of ESSENTIAL_CACHE_TOOLS) {
|
||||||
|
if (!entries.some((e) => e.name === n)) {
|
||||||
|
entries.push({ name: n, description: '(PaddedCell cache control tool)' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = renderCatalogText(
|
||||||
|
entries,
|
||||||
|
c,
|
||||||
|
'Call dynamic-cache-tools({names: [...]}) to make available tools usable this session.',
|
||||||
|
);
|
||||||
|
return { content: [{ type: 'text', text }] };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSearchToolsTool(): any {
|
||||||
|
return {
|
||||||
|
name: 'dynamic-search-tools',
|
||||||
|
description:
|
||||||
|
'Substring search (case-insensitive) over tool names + descriptions. Returns matches grouped by source.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { query: { type: 'string' } },
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
async execute(_callId: string, params: any, ctx?: ToolCtx) {
|
||||||
|
const c = ctx ?? {};
|
||||||
|
const query = String(params?.query ?? '').trim();
|
||||||
|
if (!query) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'dynamic-search-tools: query is required' }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const matched = catalog.search(query);
|
||||||
|
const text = renderCatalogText(
|
||||||
|
matched,
|
||||||
|
c,
|
||||||
|
`query: "${query}" — matches: ${matched.length}`,
|
||||||
|
);
|
||||||
|
return { content: [{ type: 'text', text }] };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCacheToolsTool(): any {
|
||||||
|
return {
|
||||||
|
name: 'dynamic-cache-tools',
|
||||||
|
description:
|
||||||
|
'Add the given tool names to this session\'s tools-cache. Takes effect starting your next turn (this iteration\'s tool list is frozen).',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
names: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
required: ['names'],
|
||||||
|
},
|
||||||
|
async execute(_callId: string, params: any, ctx?: ToolCtx) {
|
||||||
|
const c = ctx ?? {};
|
||||||
|
const names = Array.isArray(params?.names) ? params.names.map(String) : [];
|
||||||
|
if (names.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'dynamic-cache-tools: names is required (non-empty)' }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fp = cacheFilePath(c.agentId, c.sessionId);
|
||||||
|
if (!fp) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'dynamic-cache-tools: no session context' }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const cache = readCache(fp);
|
||||||
|
const known = new Set(catalog.list().map((e) => e.name));
|
||||||
|
// Essentials are always allowed; recognize them as "known" too.
|
||||||
|
for (const n of ESSENTIAL_CACHE_TOOLS) known.add(n);
|
||||||
|
const added: string[] = [];
|
||||||
|
const alreadyCached: string[] = [];
|
||||||
|
const unknown: string[] = [];
|
||||||
|
const turn = currentTurn();
|
||||||
|
for (const n of names) {
|
||||||
|
if (!known.has(n)) {
|
||||||
|
unknown.push(n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cache.cached.some((e) => e.name === n)) {
|
||||||
|
alreadyCached.push(n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cache.cached.push({ name: n, added_at_turn: turn });
|
||||||
|
added.push(n);
|
||||||
|
}
|
||||||
|
writeCache(fp, cache);
|
||||||
|
const result = {
|
||||||
|
added,
|
||||||
|
already_cached: alreadyCached,
|
||||||
|
unknown,
|
||||||
|
note: 'Newly cached tools are available starting your next turn.',
|
||||||
|
};
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEvictToolsTool(): any {
|
||||||
|
return {
|
||||||
|
name: 'dynamic-evict-tools',
|
||||||
|
description:
|
||||||
|
'Remove the given tool names from this session\'s tools-cache. Essentials are silently un-evictable. Takes effect starting your next turn.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
names: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
required: ['names'],
|
||||||
|
},
|
||||||
|
async execute(_callId: string, params: any, ctx?: ToolCtx) {
|
||||||
|
const c = ctx ?? {};
|
||||||
|
const names = Array.isArray(params?.names) ? params.names.map(String) : [];
|
||||||
|
if (names.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'dynamic-evict-tools: names is required (non-empty)' }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fp = cacheFilePath(c.agentId, c.sessionId);
|
||||||
|
if (!fp) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'dynamic-evict-tools: no session context' }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const cache = readCache(fp);
|
||||||
|
const evicted: string[] = [];
|
||||||
|
const notCached: string[] = [];
|
||||||
|
for (const n of names) {
|
||||||
|
if (ESSENTIAL_CACHE_TOOLS.has(n)) {
|
||||||
|
notCached.push(n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const idx = cache.cached.findIndex((e) => e.name === n);
|
||||||
|
if (idx < 0) {
|
||||||
|
notCached.push(n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cache.cached.splice(idx, 1);
|
||||||
|
evicted.push(n);
|
||||||
|
}
|
||||||
|
writeCache(fp, cache);
|
||||||
|
const result = {
|
||||||
|
evicted,
|
||||||
|
not_cached: notCached,
|
||||||
|
note: 'Evictions take effect starting your next turn.',
|
||||||
|
};
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user