From 2bae9e3f0d1db324d2df0fb9be28221d421ee861 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 01:37:37 +0100 Subject: [PATCH 1/8] feat(cache): add per-tool cache key prefixing and improve API clarity - Add toolName option for per-tool cache key prefixing - Add keySeparator option (default ':') - Rename keyPrefix to storeName for clarity - Rename cacheKey to cacheKeyContext (deprecated cacheKey still works) - Change keyGenerator to use object parameters - Add DEFAULT_CACHE_KEY_SEPARATOR and DEFAULT_STORE_NAME constants - Update all examples to use new API - Fix type issues in example files BREAKING CHANGE: keyPrefix renamed to storeName, default changed from 'ai-tools-cache:' to 'ai-tools-cache', keyGenerator signature changed to object parameters Fixes #91 --- packages/cache/README.md | 80 +++++--- packages/cache/src/backends/factory.ts | 27 ++- packages/cache/src/backends/index.ts | 4 +- packages/cache/src/backends/redis.ts | 30 ++- packages/cache/src/cache.ts | 131 ++++++++---- packages/cache/src/constants.ts | 6 + packages/cache/src/examples/basic-usage.ts | 109 +++++----- packages/cache/src/examples/real-world.ts | 223 ++++++++++++--------- packages/cache/src/examples/user-config.ts | 54 ++--- packages/cache/src/index.ts | 11 +- packages/cache/src/types.ts | 85 +++++--- 11 files changed, 476 insertions(+), 284 deletions(-) create mode 100644 packages/cache/src/constants.ts diff --git a/packages/cache/README.md b/packages/cache/README.md index 6f79af2..1e4509b 100644 --- a/packages/cache/README.md +++ b/packages/cache/README.md @@ -87,7 +87,7 @@ import { createCached } from '@ai-sdk-tools/cache'; const cached = createCached({ cache: Redis.createClient({ url: "redis://localhost:6379" }), - keyPrefix: "my-app:", + storeName: "my-app", ttl: 30 * 60 * 1000, }); ``` @@ -100,7 +100,7 @@ import { createCached } from '@ai-sdk-tools/cache'; const cached = createCached({ cache: new IORedis("redis://localhost:6379"), - keyPrefix: "my-app:", + storeName: "my-app", ttl: 30 * 60 * 1000, }); ``` @@ -124,22 +124,57 @@ Supported clients: ```typescript const cached = createCached({ - cache?: any; // Redis client (optional, defaults to LRU) - keyPrefix?: string; // Cache key prefix (default: "ai-tools-cache:") - ttl?: number; // Time to live in ms (default: 10min LRU, 30min Redis) - debug?: boolean; // Debug logging (default: false) - onHit?: (key: string) => void; // Cache hit callback - onMiss?: (key: string) => void; // Cache miss callback + cache?: any; // Redis client (optional, defaults to LRU) + storeName?: string; // Redis store name (default: "ai-tools-cache") + ttl?: number; // Time to live in ms (default: 10min LRU, 30min Redis) + toolName?: string; // Tool name prefix for cache keys + keySeparator?: string; // Separator between key components (default: ':') + cacheKeyContext?: () => string; // Generate dynamic context suffix (multi-tenant) + debug?: boolean; // Debug logging (default: false) + onHit?: (key: string) => void; // Cache hit callback + onMiss?: (key: string) => void; // Cache miss callback }); ``` +**`toolName`** - Prefix cache keys with tool name to prevent collisions between tools using similar parameters. + +**`keySeparator`** - Customize separator between key components (default: `':'`). Useful for specific storage backend requirements. + +**`cacheKeyContext`** - Function to generate dynamic context suffix for multi-tenant apps. The context is appended to the end of the cache key with a separator in between. Isolates cache by user, team, or other contextual data. + +## Per-Tool Cache Key Prefixing + +When multiple tools use similar parameters, they can create identical cache keys causing collisions. Use `toolName` to namespace your cache keys: + +```typescript +const cached = createCached({ cache: Redis.fromEnv() }); + +const weatherTool = cached(originalWeatherTool, { + toolName: 'weatherTool', +}); + +const translationTool = cached(originalTranslationTool, { + toolName: 'translationTool', +}); +``` + +This creates a clear Redis key hierarchy: + +``` +// Without toolName (collision risk) +ai-tools-cache:{city:"NYC"} + +// With toolName (no collisions) +ai-tools-cache:weatherTool:{city:"NYC"} +ai-tools-cache:translationTool:{city:"NYC"} +``` + ## Multi-Tenant Apps (Context-Aware Caching) -For apps with user/team context, just add `getContext` to the cache config: +For multi-tenant apps, isolate cache by user/team using `cacheKeyContext`: ```typescript import { cached } from '@ai-sdk-tools/cache'; -// Your app's context system (could be React context, global state, etc.) const burnRateAnalysisTool = tool({ description: 'Analyze burn rate', @@ -148,24 +183,21 @@ const burnRateAnalysisTool = tool({ to: z.string(), }), execute: async ({ from, to }) => { - // Your app's way of getting current user/team context - const currentUser = getCurrentUser(); // or useUser(), getSession(), etc. - + const currentUser = getCurrentUser(); return await db.getBurnRate({ - teamId: currentUser.teamId, // ← Context used here + teamId: currentUser.teamId, from, to, }); }, }); -// Cache with context - that's it! export const cachedBurnRateTool = cached(burnRateAnalysisTool, { - cacheKey: () => { + cacheKeyContext: () => { const currentUser = getCurrentUser(); return `team:${currentUser.teamId}:user:${currentUser.id}`; }, - ttl: 30 * 60 * 1000, // 30 minutes + ttl: 30 * 60 * 1000, }); ``` @@ -185,15 +217,16 @@ const cacheBackend = createCacheBackend({ type: 'redis', redis: { client: Redis.createClient({ url: process.env.REDIS_URL }), - keyPrefix: 'my-app:', + storeName: 'my-app', }, }); // Export configured cache function -export function cached(tool: T, options = {}) { +export function cached(tool: T, toolName?: string, options = {}) { return baseCached(tool, { store: cacheBackend, - cacheKey: () => { + toolName, // Per-tool prefix + cacheKeyContext: () => { const currentUser = getCurrentUser(); return `team:${currentUser.teamId}:user:${currentUser.id}`; }, @@ -203,9 +236,10 @@ export function cached(tool: T, options = {}) { }); } -// Throughout your app +// Throughout your app - each tool gets its own prefix import { cached } from '@/lib/cache'; -export const myTool = cached(originalTool); +export const weatherTool = cached(originalWeatherTool, 'weatherTool'); +export const translateTool = cached(originalTranslateTool, 'translateTool'); ``` ## Streaming Tools with Artifacts @@ -335,4 +369,4 @@ Contributions are welcome! Please read our [contributing guide](../../CONTRIBUTI ## License -MIT Ā© [AI SDK Tools](https://github.com/ai-sdk-tools/ai-sdk-tools) \ No newline at end of file +MIT Ā© [AI SDK Tools](https://github.com/ai-sdk-tools/ai-sdk-tools) diff --git a/packages/cache/src/backends/factory.ts b/packages/cache/src/backends/factory.ts index c4702ec..f66aa71 100644 --- a/packages/cache/src/backends/factory.ts +++ b/packages/cache/src/backends/factory.ts @@ -1,5 +1,5 @@ -import type { CacheStore } from "../types"; import { LRUCacheStore, SimpleCacheStore } from "../cache-store"; +import type { CacheStore } from "../types"; import { MemoryCacheStore } from "./memory"; import { RedisCacheStore } from "./redis"; @@ -12,45 +12,50 @@ export interface CacheBackendConfig { defaultTTL?: number; redis?: { client: any; - keyPrefix?: string; + storeName?: string; }; } /** * Factory function to create cache backends */ -export function createCacheBackend(config: CacheBackendConfig): CacheStore { +export function createCacheBackend( + config: CacheBackendConfig, +): CacheStore { let store: CacheStore; - + switch (config.type) { case "memory": store = new MemoryCacheStore(config.maxSize); break; - + case "lru": store = new LRUCacheStore(config.maxSize); break; - + case "simple": store = new SimpleCacheStore(config.maxSize); break; - + case "redis": if (!config.redis?.client) { throw new Error("Redis client is required for redis cache backend"); } - store = new RedisCacheStore(config.redis.client, config.redis.keyPrefix); + store = new RedisCacheStore( + config.redis.client, + config.redis.storeName, + ); break; - + default: throw new Error(`Unknown cache backend type: ${(config as any).type}`); } - + // Add default TTL support if configured if (config.defaultTTL) { (store as any).getDefaultTTL = () => config.defaultTTL; } - + return store; } diff --git a/packages/cache/src/backends/index.ts b/packages/cache/src/backends/index.ts index 246c760..8f81f83 100644 --- a/packages/cache/src/backends/index.ts +++ b/packages/cache/src/backends/index.ts @@ -1,4 +1,4 @@ export { LRUCacheStore, SimpleCacheStore } from "../cache-store"; -export { RedisCacheStore } from "./redis"; -export { MemoryCacheStore } from "./memory"; export { createCacheBackend } from "./factory"; +export { MemoryCacheStore } from "./memory"; +export { RedisCacheStore } from "./redis"; diff --git a/packages/cache/src/backends/redis.ts b/packages/cache/src/backends/redis.ts index ba6eba1..f980614 100644 --- a/packages/cache/src/backends/redis.ts +++ b/packages/cache/src/backends/redis.ts @@ -1,3 +1,7 @@ +import { + DEFAULT_CACHE_KEY_SEPARATOR, + DEFAULT_STORE_NAME, +} from "../constants"; import type { CacheEntry, CacheStore } from "../types"; /** @@ -8,9 +12,13 @@ export class RedisCacheStore implements CacheStore { private redis: any; private keyPrefix: string; - constructor(redisClient: any, keyPrefix = "ai-tools-cache:") { + constructor(redisClient: any, storeName = DEFAULT_STORE_NAME) { this.redis = redisClient; - this.keyPrefix = keyPrefix; + // Append separator if storeName doesn't end with a common separator + const endsWithSeparator = /[:|\-_]$/.test(storeName); + this.keyPrefix = storeName && !endsWithSeparator + ? `${storeName}${DEFAULT_CACHE_KEY_SEPARATOR}` + : storeName; } private getKey(key: string): string { @@ -21,12 +29,12 @@ export class RedisCacheStore implements CacheStore { try { const data = await this.redis.get(this.getKey(key)); if (!data) return undefined; - + // Handle different Redis client return types let jsonString: string; - if (typeof data === 'string') { + if (typeof data === "string") { jsonString = data; - } else if (typeof data === 'object') { + } else if (typeof data === "object") { // Some Redis clients return objects directly return { result: data.result, @@ -37,7 +45,7 @@ export class RedisCacheStore implements CacheStore { // Convert other types to string jsonString = String(data); } - + const parsed = JSON.parse(jsonString); return { result: parsed.result, @@ -57,21 +65,25 @@ export class RedisCacheStore implements CacheStore { timestamp: entry.timestamp, key: entry.key, }); - + await this.redis.set(this.getKey(key), data); } catch (error) { console.warn(`Redis cache set error for key ${key}:`, error); } } - async setWithTTL(key: string, entry: CacheEntry, ttlSeconds: number): Promise { + async setWithTTL( + key: string, + entry: CacheEntry, + ttlSeconds: number, + ): Promise { try { const data = JSON.stringify({ result: entry.result, timestamp: entry.timestamp, key: entry.key, }); - + await this.redis.setex(this.getKey(key), ttlSeconds, data); } catch (error) { console.warn(`Redis cache setex error for key ${key}:`, error); diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 3802324..f4cac9c 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -1,25 +1,60 @@ import type { Tool } from "ai"; -import { createCacheBackend } from "./backends/factory"; +import { createCacheBackend } from "./backends"; import { LRUCacheStore } from "./cache-store"; +import {DEFAULT_CACHE_KEY_SEPARATOR, DEFAULT_STORE_NAME} from "./constants"; import type { CachedTool, CacheOptions, CacheStats, CacheStore } from "./types"; /** - * Default cache key generator - stable and deterministic + * Default cache key generator - stable and deterministic. + * Example: "weatherTool:{city:NYC}:team:123" */ -function defaultKeyGenerator(params: any, context?: any): string { - const paramsKey = serializeValue(params); +function defaultKeyGenerator(options: { + params: any; + context?: any; + toolName?: string; + keySeparator?: string; +}): string { + const { + params, + context, + toolName, + keySeparator = DEFAULT_CACHE_KEY_SEPARATOR, + } = options; + const parts: string[] = []; + + if (toolName != null) { + parts.push(toolName); + } + + parts.push(serializeValue(params)); - if (context) { - return `${paramsKey}|${context}`; + if (context != null) { + parts.push(context); } - return paramsKey; + return parts.join(keySeparator); } /** - * Serialize a value to a stable string representation + * Serialize a value to a stable string representation. + * Useful for custom keyGenerator functions to ensure consistent serialization. + * + * @param value - Any value to serialize (primitives, objects, arrays, dates) + * @returns Stable string representation of the value + * + * @example + * ```typescript + * import { serializeValue } from '@ai-sdk-tools/cache'; + * + * const customKeyGenerator = (params, context, toolName, separator) => { + * const serialized = serializeValue(params); + * return `custom:${toolName}${separator}${serialized}`; + * }; + * ``` + * + * @public */ -function serializeValue(value: any): string { +export function serializeValue(value: any): string { // Handle different parameter types like React Query if (value === null || value === undefined) { return "null"; @@ -66,12 +101,18 @@ function createStreamingCachedTool( store, keyGenerator = defaultKeyGenerator, cacheKey, + cacheKeyContext, + toolName, + keySeparator = DEFAULT_CACHE_KEY_SEPARATOR, shouldCache = () => true, onHit, onMiss, debug = false, } = options; + // For backward compatibility, support both cacheKeyContext and cacheKey + const contextFn = cacheKeyContext ?? cacheKey; + const cacheStore = store || new LRUCacheStore(maxSize); let hits = 0; let misses = 0; @@ -81,9 +122,8 @@ function createStreamingCachedTool( ...tool, execute: async function* (...args: any[]) { const [params, executionOptions] = args; - // Get context from cacheKey function - const context = cacheKey?.(); - const key = keyGenerator(params, context); + const context = contextFn?.(); + const key = keyGenerator({ params, context, toolName, keySeparator }); const now = Date.now(); // Check cache first @@ -291,9 +331,9 @@ function createStreamingCachedTool( cacheStore.clear(); } }, - async isCached(params: any) { - const context = cacheKey?.(); - const key = keyGenerator(params, context); + async isCached(params: any): Promise { + const context = contextFn?.(); + const key = keyGenerator({ params, context, toolName, keySeparator }); const cached = await cacheStore.get(key); if (!cached) return false; @@ -307,9 +347,9 @@ function createStreamingCachedTool( return true; }, - getCacheKey(params: any) { - const context = cacheKey?.(); - return keyGenerator(params, context); + getCacheKey(params: any): string { + const context = contextFn?.(); + return keyGenerator({ params, context, toolName, keySeparator }); }, } as unknown as CachedTool; } @@ -328,12 +368,18 @@ export function cached( store, keyGenerator = defaultKeyGenerator, cacheKey, + cacheKeyContext, + toolName, + keySeparator = DEFAULT_CACHE_KEY_SEPARATOR, shouldCache = () => true, onHit, onMiss, debug = false, } = options || {}; + // For backward compatibility, support both cacheKeyContext and cacheKey + const contextFn = cacheKeyContext ?? cacheKey; + const cacheStore = store || new LRUCacheStore(maxSize); const effectiveTTL = ttl ?? cacheStore.getDefaultTTL?.() ?? 5 * 60 * 1000; let hits = 0; @@ -365,8 +411,8 @@ export function cached( }, async isCached(params: any): Promise { - const context = cacheKey?.(); - const key = keyGenerator(params, context); + const context = contextFn?.(); + const key = keyGenerator({ params, context, toolName, keySeparator }); const cached = await cacheStore.get(key); if (!cached) return false; @@ -382,8 +428,8 @@ export function cached( }, getCacheKey(params: any): string { - const context = cacheKey?.(); - return keyGenerator(params, context); + const context = contextFn?.(); + return keyGenerator({ params, context, toolName, keySeparator }); }, }; @@ -394,8 +440,13 @@ export function cached( if (target.execute?.constructor?.name === "AsyncGeneratorFunction") { return async function* (...args: any[]) { const [params, executionOptions] = args; - const context = cacheKey?.(); - const key = keyGenerator(params, context); + const context = contextFn?.(); + const key = keyGenerator({ + params, + context, + toolName, + keySeparator, + }); const now = Date.now(); // Check cache @@ -522,8 +573,13 @@ export function cached( // Regular async function return async (...args: any[]) => { const [params, executionOptions] = args; - const context = cacheKey?.(); - const key = keyGenerator(params, context); + const context = contextFn?.(); + const key = keyGenerator({ + params, + context, + toolName, + keySeparator, + }); const now = Date.now(); // Check cache @@ -586,16 +642,10 @@ export function createCachedFunction( * Cache multiple tools with the same configuration */ export function cacheTools>( - tools: T, + tools: TTools, options: CacheOptions = {}, ): { [K in keyof TTools]: CachedTool } { - const cachedTools = {} as { [K in keyof TTools]: CachedTool }; - - for (const [name, tool] of Object.entries(tools)) { - cachedTools[name as keyof TTools] = cached(tool, options); - } - - return cachedTools; + return Object.fromEntries(Object.keys(tools).map((name) => ([[name as keyof TTools], cached(tools[name], options)]))) } /** @@ -619,10 +669,13 @@ export function cacheTools>( export function createCached( options: { cache?: any; // User's Redis client - we pass it directly - keyPrefix?: string; + storeName?: string; ttl?: number; debug?: boolean; cacheKey?: () => string; + cacheKeyContext?: () => string; + toolName?: string; + keySeparator?: string; onHit?: (key: string) => void; onMiss?: (key: string) => void; } = {}, @@ -638,6 +691,9 @@ export function createCached( return createCachedFunction(lruStore, { debug: options.debug || false, cacheKey: options.cacheKey, + cacheKeyContext: options.cacheKeyContext, + toolName: options.toolName, + keySeparator: options.keySeparator, onHit: options.onHit, onMiss: options.onMiss, }); @@ -649,13 +705,16 @@ export function createCached( defaultTTL: options.ttl || 30 * 60 * 1000, // 30 minutes default redis: { client: options.cache, // Pass user's Redis client directly - keyPrefix: options.keyPrefix || "ai-tools-cache:", + storeName: options.storeName || DEFAULT_STORE_NAME, }, }); return createCachedFunction(redisStore, { debug: options.debug || false, cacheKey: options.cacheKey, + cacheKeyContext: options.cacheKeyContext, + toolName: options.toolName, + keySeparator: options.keySeparator, onHit: options.onHit, onMiss: options.onMiss, }); diff --git a/packages/cache/src/constants.ts b/packages/cache/src/constants.ts new file mode 100644 index 0000000..6952a11 --- /dev/null +++ b/packages/cache/src/constants.ts @@ -0,0 +1,6 @@ +/** + * Default separator used between cache key parts. + * Colon is ideal for Redis hierarchy visualization. + */ +export const DEFAULT_CACHE_KEY_SEPARATOR = ":"; +export const DEFAULT_STORE_NAME = "ai-tools-cache"; diff --git a/packages/cache/src/examples/basic-usage.ts b/packages/cache/src/examples/basic-usage.ts index 3754640..dc12f90 100644 --- a/packages/cache/src/examples/basic-usage.ts +++ b/packages/cache/src/examples/basic-usage.ts @@ -5,16 +5,16 @@ import { cached, cacheTools } from "../index"; // Example 1: Simple weather tool with caching const expensiveWeatherTool = tool({ description: "Get current weather information for a location", - parameters: z.object({ + inputSchema: z.object({ location: z.string().describe("The location to get weather for"), units: z.enum(["celsius", "fahrenheit"]).optional().default("celsius"), }), execute: async ({ location, units }) => { console.log(`šŸŒ¤ļø Making expensive API call for ${location}...`); - + // Simulate expensive API call - await new Promise(resolve => setTimeout(resolve, 1000)); - + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Mock weather data const mockData = { location, @@ -24,7 +24,7 @@ const expensiveWeatherTool = tool({ windSpeed: 10, timestamp: new Date().toISOString(), }; - + return mockData; }, }); @@ -38,16 +38,18 @@ const weatherTool = cached(expensiveWeatherTool, { // Example 2: Financial analysis tool const burnRateAnalysisTool = tool({ description: "Analyze company burn rate and financial health", - parameters: z.object({ + inputSchema: z.object({ companyId: z.string(), months: z.number().min(1).max(24), }), execute: async ({ companyId, months }) => { - console.log(`šŸ“Š Analyzing burn rate for company ${companyId} over ${months} months...`); - + console.log( + `šŸ“Š Analyzing burn rate for company ${companyId} over ${months} months...`, + ); + // Simulate heavy computation - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Mock analysis result return { companyId, @@ -67,8 +69,8 @@ const burnRateAnalysisTool = tool({ // Cache with custom configuration const cachedBurnRateTool = cached(burnRateAnalysisTool, { ttl: 30 * 60 * 1000, // 30 minutes - keyGenerator: ({ companyId, months }) => `burnrate:${companyId}:${months}`, - shouldCache: (params, result) => { + keyGenerator: ({ params }) => `burnrate:${params.companyId}:${params.months}`, + shouldCache: (_params, result) => { // Only cache successful analyses return result && !result.error; }, @@ -79,15 +81,15 @@ const cachedBurnRateTool = cached(burnRateAnalysisTool, { // Example 3: Multiple tools with same cache config const calculatorTool = tool({ description: "Perform mathematical calculations", - parameters: z.object({ + inputSchema: z.object({ expression: z.string(), }), execute: async ({ expression }) => { console.log(`🧮 Calculating: ${expression}`); - + // Simulate some processing time - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + try { // Simple expression evaluation (use a proper math parser in production) const result = Function(`"use strict"; return (${expression})`)(); @@ -100,16 +102,16 @@ const calculatorTool = tool({ const databaseTool = tool({ description: "Query database for information", - parameters: z.object({ + inputSchema: z.object({ query: z.string(), table: z.string(), }), execute: async ({ query, table }) => { console.log(`šŸ—„ļø Querying ${table}: ${query}`); - + // Simulate database query - await new Promise(resolve => setTimeout(resolve, 500)); - + await new Promise((resolve) => setTimeout(resolve, 500)); + return { query, table, @@ -123,13 +125,16 @@ const databaseTool = tool({ }); // Cache multiple tools with same config -const { calculator, database } = cacheTools({ - calculator: calculatorTool, - database: databaseTool, -}, { - ttl: 5 * 60 * 1000, // 5 minutes - debug: true, -}); +const { calculator, database } = cacheTools( + { + calculator: calculatorTool, + database: databaseTool, + }, + { + ttl: 5 * 60 * 1000, // 5 minutes + debug: true, + }, +); /** * Demo function to show caching in action @@ -139,17 +144,17 @@ export async function demonstrateCache() { // Test weather tool caching console.log("=== Weather Tool Demo ==="); - + console.log("First call (should be slow):"); - const weather1 = await weatherTool.execute({ location: "New York" }); + const weather1 = await weatherTool.execute?.({ location: "New York", units: 'fahrenheit' }, {toolCallId: 'weatherTool', messages: []}); console.log("Result:", weather1); - + console.log("\nSecond call with same params (should be fast):"); - const weather2 = await weatherTool.execute({ location: "New York" }); + const weather2 = await weatherTool.execute?.({ location: "New York", units: 'fahrenheit' }, {toolCallId: 'weatherTool', messages: []}); console.log("Result:", weather2); - + console.log("\nThird call with different params (should be slow):"); - const weather3 = await weatherTool.execute({ location: "London" }); + const weather3 = await weatherTool.execute?.({ location: "London", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); console.log("Result:", weather3); // Show cache stats @@ -157,30 +162,30 @@ export async function demonstrateCache() { // Test burn rate tool console.log("\n=== Burn Rate Analysis Demo ==="); - - const analysis1 = await cachedBurnRateTool.execute({ - companyId: "company-123", - months: 12 - }); + + const analysis1 = await cachedBurnRateTool.execute?.({ + companyId: "company-123", + months: 12, + }, {toolCallId: 'cachedBurnRateTool', messages: []}); console.log("Analysis result:", analysis1); - + // Same params - should hit cache - const analysis2 = await cachedBurnRateTool.execute({ - companyId: "company-123", - months: 12 - }); + const analysis2 = await cachedBurnRateTool.execute?.({ + companyId: "company-123", + months: 12, + }, {toolCallId: 'cachedBurnRateTool', messages: []}); console.log("Cached analysis:", analysis2); console.log("\nBurn rate tool cache stats:", cachedBurnRateTool.getStats()); // Test multiple tools console.log("\n=== Multiple Tools Demo ==="); - - await calculator.execute({ expression: "15 * 8" }); - await calculator.execute({ expression: "15 * 8" }); // Should hit cache - - await database.execute({ query: "SELECT * FROM users", table: "users" }); - await database.execute({ query: "SELECT * FROM users", table: "users" }); // Should hit cache + + await calculator.execute?.({ expression: "15 * 8" }, {toolCallId: 'calculator', messages: []}); + await calculator.execute?.({ expression: "15 * 8" }, {toolCallId: 'calculator', messages: []}); // Should hit cache + + await database.execute?.({ query: "SELECT * FROM users", table: "users" }, {toolCallId: 'database', messages: []}); + await database.execute?.({ query: "SELECT * FROM users", table: "users" }, {toolCallId: 'database', messages: []}); // Should hit cache console.log("\nCalculator cache stats:", calculator.getStats()); console.log("Database cache stats:", database.getStats()); @@ -197,9 +202,9 @@ export async function demonstrateCacheManagement() { const tool = cached(weatherTool, { debug: true }); // Add some entries - await tool.execute({ location: "Paris" }); - await tool.execute({ location: "Tokyo" }); - await tool.execute({ location: "Sydney" }); + await tool.execute?.({ location: "Paris", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); + await tool.execute?.({ location: "Tokyo", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); + await tool.execute?.({ location: "Sydney", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); console.log("Cache stats after adding entries:", tool.getStats()); diff --git a/packages/cache/src/examples/real-world.ts b/packages/cache/src/examples/real-world.ts index 780b741..75719f5 100644 --- a/packages/cache/src/examples/real-world.ts +++ b/packages/cache/src/examples/real-world.ts @@ -6,7 +6,7 @@ import { cached } from "../"; const weatherApiTool = cached( tool({ description: "Get real weather data from external API", - parameters: z.object({ + inputSchema: z.object({ location: z.string(), units: z.enum(["metric", "imperial"]).default("metric"), }), @@ -14,40 +14,41 @@ const weatherApiTool = cached( // In real app, this would be an actual API call const apiKey = process.env.WEATHER_API_KEY; const response = await fetch( - `https://api.openweathermap.org/data/2.5/weather?q=${location}&units=${units}&appid=${apiKey}` + `https://api.openweathermap.org/data/2.5/weather?q=${location}&units=${units}&appid=${apiKey}`, ); - + if (!response.ok) { throw new Error(`Weather API error: ${response.statusText}`); } - + return response.json(); }, }), { ttl: 10 * 60 * 1000, // 10 minutes - weather doesn't change that fast - keyGenerator: ({ location, units }) => `weather:${location.toLowerCase()}:${units}`, + keyGenerator: ({ params }) => + `weather:${params.location.toLowerCase()}:${params.units}`, shouldCache: (_, result) => { // Don't cache errors return !result.error && result.main; }, onHit: (key) => console.log(`šŸ’° Saved API call: ${key}`), onMiss: (key) => console.log(`🌐 Making API call: ${key}`), - } + }, ); // Example 2: Database query caching const userProfileTool = cached( tool({ description: "Get user profile information", - parameters: z.object({ + inputSchema: z.object({ userId: z.string(), includePreferences: z.boolean().default(false), }), execute: async ({ userId, includePreferences }) => { // Simulate database query console.log(`šŸ—„ļø Querying database for user ${userId}`); - + // In real app, this would be a database query const baseProfile = { id: userId, @@ -58,7 +59,7 @@ const userProfileTool = cached( if (includePreferences) { // Additional expensive query - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); return { ...baseProfile, preferences: { @@ -74,27 +75,29 @@ const userProfileTool = cached( }), { ttl: 5 * 60 * 1000, // 5 minutes - user data changes occasionally - keyGenerator: ({ userId, includePreferences }) => - `user:${userId}:prefs:${includePreferences}`, + keyGenerator: ({ params }) => + `user:${params.userId}:prefs:${params.includePreferences}`, onHit: () => console.log("šŸ’¾ Database query avoided"), - } + }, ); // Example 3: Heavy computation caching const financialAnalysisTool = cached( tool({ description: "Perform complex financial analysis", - parameters: z.object({ + inputSchema: z.object({ companyId: z.string(), analysisType: z.enum(["burnRate", "growth", "profitability"]), timeframe: z.number().min(1).max(36), // months }), execute: async ({ companyId, analysisType, timeframe }) => { - console.log(`šŸ“Š Running ${analysisType} analysis for ${companyId} over ${timeframe} months`); - + console.log( + `šŸ“Š Running ${analysisType} analysis for ${companyId} over ${timeframe} months`, + ); + // Simulate heavy computation - await new Promise(resolve => setTimeout(resolve, 3000)); - + await new Promise((resolve) => setTimeout(resolve, 3000)); + // Mock complex analysis result const baseMetrics = { companyId, @@ -133,33 +136,33 @@ const financialAnalysisTool = cached( }), { ttl: 60 * 60 * 1000, // 1 hour - financial analysis is expensive - keyGenerator: ({ companyId, analysisType, timeframe }) => - `analysis:${companyId}:${analysisType}:${timeframe}`, + keyGenerator: ({ params }) => + `analysis:${params.companyId}:${params.analysisType}:${params.timeframe}`, shouldCache: (_, result) => { // Only cache successful analyses return result && !result.error && result.generatedAt; }, onHit: (key) => console.log(`šŸŽÆ Avoided expensive computation: ${key}`), onMiss: (key) => console.log(`⚔ Running expensive computation: ${key}`), - } + }, ); // Example 4: Translation service with long-term caching const translationTool = cached( tool({ description: "Translate text to different languages", - parameters: z.object({ + inputSchema: z.object({ text: z.string(), targetLanguage: z.string(), sourceLanguage: z.string().default("auto"), }), execute: async ({ text, targetLanguage, sourceLanguage }) => { console.log(`šŸŒ Translating "${text}" to ${targetLanguage}`); - + // In real app, this would call a translation API // Simulate API call - await new Promise(resolve => setTimeout(resolve, 800)); - + await new Promise((resolve) => setTimeout(resolve, 800)); + // Mock translation result return { originalText: text, @@ -172,36 +175,41 @@ const translationTool = cached( }), { ttl: 24 * 60 * 60 * 1000, // 24 hours - translations don't change - keyGenerator: ({ text, targetLanguage, sourceLanguage }) => { + keyGenerator: ({ params }) => { // Use base64 encoding for text to handle special characters - const encodedText = Buffer.from(text).toString("base64"); - return `translate:${encodedText}:${sourceLanguage}:${targetLanguage}`; + const encodedText = Buffer.from(params.text).toString("base64"); + return `translate:${encodedText}:${params.sourceLanguage}:${params.targetLanguage}`; }, maxSize: 5000, // Store more translations - onHit: (key) => console.log(`šŸ’¬ Translation cache hit: ${key.slice(0, 50)}...`), - } + onHit: (key) => + console.log(`šŸ’¬ Translation cache hit: ${key.slice(0, 50)}...`), + }, ); // Example 5: File processing with conditional caching const documentAnalysisTool = cached( tool({ description: "Analyze document content and extract insights", - parameters: z.object({ + inputSchema: z.object({ documentId: z.string(), analysisDepth: z.enum(["basic", "detailed", "comprehensive"]), }), execute: async ({ documentId, analysisDepth }) => { - console.log(`šŸ“„ Analyzing document ${documentId} with ${analysisDepth} depth`); - + console.log( + `šŸ“„ Analyzing document ${documentId} with ${analysisDepth} depth`, + ); + // Simulate document processing time based on depth const processingTime = { basic: 500, detailed: 2000, comprehensive: 5000, }; - - await new Promise(resolve => setTimeout(resolve, processingTime[analysisDepth])); - + + await new Promise((resolve) => + setTimeout(resolve, processingTime[analysisDepth]), + ); + const baseAnalysis = { documentId, analysisDepth, @@ -242,15 +250,15 @@ const documentAnalysisTool = cached( }), { ttl: 30 * 60 * 1000, // 30 minutes - keyGenerator: ({ documentId, analysisDepth }) => - `doc:${documentId}:${analysisDepth}`, - shouldCache: (params, result) => { + keyGenerator: ({ params }) => + `doc:${params.documentId}:${params.analysisDepth}`, + shouldCache: (toolParams, result) => { // Only cache successful analyses for detailed/comprehensive - return result && !result.error && params.analysisDepth !== "basic"; + return result && !result.error && toolParams.analysisDepth !== "basic"; }, onHit: (key) => console.log(`šŸ“‹ Document analysis cache hit: ${key}`), onMiss: (key) => console.log(`šŸ” Processing document: ${key}`), - } + }, ); /** @@ -261,65 +269,92 @@ export async function demonstrateRealWorldCaching() { // Weather API caching console.log("=== Weather API Caching ==="); - await weatherApiTool.execute({ location: "New York" }); - await weatherApiTool.execute({ location: "New York" }); // Cache hit - await weatherApiTool.execute({ location: "New York", units: "imperial" }); // Different cache key - + await weatherApiTool.execute?.( + { location: "New York", units: "metric" }, + { toolCallId: "weatherApiTool", messages: [] }, + ); + await weatherApiTool.execute?.( + { location: "New York", units: "metric" }, + { toolCallId: "weatherApiTool", messages: [] }, + ); // Cache hit + await weatherApiTool.execute?.( + { location: "New York", units: "imperial" }, + { toolCallId: "weatherApiTool", messages: [] }, + ); // Different cache key + console.log("Weather tool stats:", weatherApiTool.getStats()); // User profile caching console.log("\n=== User Profile Caching ==="); - await userProfileTool.execute({ userId: "user123" }); - await userProfileTool.execute({ userId: "user123" }); // Cache hit - await userProfileTool.execute({ userId: "user123", includePreferences: true }); // Different cache key - + await userProfileTool.execute?.( + { userId: "user123", includePreferences: false }, + { toolCallId: "userProfileTool", messages: [] }, + ); + await userProfileTool.execute?.( + { userId: "user123", includePreferences: false }, + { toolCallId: "userProfileTool", messages: [] }, + ); // Cache hit + await userProfileTool.execute?.( + { userId: "user123", includePreferences: true }, + { toolCallId: "userProfileTool", messages: [] }, + ); // Different cache key + console.log("User profile tool stats:", userProfileTool.getStats()); // Financial analysis caching console.log("\n=== Financial Analysis Caching ==="); - await financialAnalysisTool.execute({ - companyId: "company-abc", - analysisType: "burnRate", - timeframe: 12 - }); - await financialAnalysisTool.execute({ - companyId: "company-abc", - analysisType: "burnRate", - timeframe: 12 - }); // Cache hit - saves 3 seconds! - - console.log("Financial analysis tool stats:", financialAnalysisTool.getStats()); + await financialAnalysisTool.execute?.( + { companyId: "company-abc", analysisType: "burnRate", timeframe: 12 }, + { toolCallId: "financialAnalysisTool", messages: [] }, + ); + await financialAnalysisTool.execute?.( + { companyId: "company-abc", analysisType: "burnRate", timeframe: 12 }, + { toolCallId: "financialAnalysisTool", messages: [] }, + ); // Cache hit - saves 3 seconds! + + console.log( + "Financial analysis tool stats:", + financialAnalysisTool.getStats(), + ); // Translation caching console.log("\n=== Translation Caching ==="); - await translationTool.execute({ - text: "Hello, how are you?", - targetLanguage: "es" - }); - await translationTool.execute({ - text: "Hello, how are you?", - targetLanguage: "es" - }); // Cache hit - + await translationTool.execute?.( + { + text: "Hello, how are you?", + targetLanguage: "es", + sourceLanguage: "auto", + }, + { toolCallId: "translationTool", messages: [] }, + ); + await translationTool.execute?.( + { + text: "Hello, how are you?", + targetLanguage: "es", + sourceLanguage: "auto", + }, + { toolCallId: "translationTool", messages: [] }, + ); // Cache hit + console.log("Translation tool stats:", translationTool.getStats()); // Document analysis with conditional caching console.log("\n=== Document Analysis Caching ==="); - await documentAnalysisTool.execute({ - documentId: "doc123", - analysisDepth: "basic" - }); // Won't be cached (basic analysis) - - await documentAnalysisTool.execute({ - documentId: "doc123", - analysisDepth: "detailed" - }); // Will be cached - - await documentAnalysisTool.execute({ - documentId: "doc123", - analysisDepth: "detailed" - }); // Cache hit - + await documentAnalysisTool.execute?.( + { documentId: "doc123", analysisDepth: "basic" }, + { toolCallId: "documentAnalysisTool", messages: [] }, + ); // Won't be cached (basic analysis) + + await documentAnalysisTool.execute?.( + { documentId: "doc123", analysisDepth: "detailed" }, + { toolCallId: "documentAnalysisTool", messages: [] }, + ); // Will be cached + + await documentAnalysisTool.execute?.( + { documentId: "doc123", analysisDepth: "detailed" }, + { toolCallId: "documentAnalysisTool", messages: [] }, + ); // Cache hit + console.log("Document analysis tool stats:", documentAnalysisTool.getStats()); console.log("\nāœ… Real-world caching demo complete!"); @@ -334,32 +369,38 @@ export async function demonstratePerformanceGains() { // Create uncached version for comparison const uncachedAnalysisTool = tool({ description: "Uncached financial analysis", - parameters: z.object({ + inputSchema: z.object({ companyId: z.string(), analysisType: z.enum(["burnRate"]), + timeframe: z.number().min(1).max(36), }), execute: async ({ companyId, analysisType }) => { console.log(`🐌 Running uncached analysis for ${companyId}`); - await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay + await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 second delay return { companyId, analysisType, result: "analysis complete" }; }, }); - const params = { companyId: "test-company", analysisType: "burnRate" as const }; + const params = { + companyId: "test-company", + analysisType: "burnRate" as const, + timeframe: 12, + }; + const executeOptions = { toolCallId: "analysisTools", messages: [] }; // Test uncached performance console.log("Testing uncached tool (2 calls):"); const uncachedStart = Date.now(); - await uncachedAnalysisTool.execute(params); - await uncachedAnalysisTool.execute(params); + await uncachedAnalysisTool.execute?.(params, executeOptions); + await uncachedAnalysisTool.execute?.(params, executeOptions); const uncachedTime = Date.now() - uncachedStart; console.log(`Uncached total time: ${uncachedTime}ms\n`); // Test cached performance console.log("Testing cached tool (2 calls):"); const cachedStart = Date.now(); - await financialAnalysisTool.execute(params); - await financialAnalysisTool.execute(params); // This should be instant + await financialAnalysisTool.execute?.(params, executeOptions); + await financialAnalysisTool.execute?.(params, executeOptions); // This should be instant const cachedTime = Date.now() - cachedStart; console.log(`Cached total time: ${cachedTime}ms`); diff --git a/packages/cache/src/examples/user-config.ts b/packages/cache/src/examples/user-config.ts index b7e17d6..0cdaeff 100644 --- a/packages/cache/src/examples/user-config.ts +++ b/packages/cache/src/examples/user-config.ts @@ -3,32 +3,34 @@ * This is the recommended pattern for configuring cache backends */ -import { cached as baseCached } from '../index'; -import { createCacheBackend } from '../backends/factory'; -import type { CacheOptions } from '../index'; -import type { Tool } from 'ai'; -import Redis from 'redis'; // User installs this +import type { Tool } from "ai"; +import Redis from "redis"; // User installs this +import { createCacheBackend } from "../backends"; +import type { CacheOptions } from "../index"; +import { cached as baseCached } from "../index"; // ===== User's cache configuration file (e.g., src/lib/cache.ts) ===== // 1. Create your cache backend const redisClient = Redis.createClient({ - host: 'localhost', - port: 6379, + socket: { + host: "localhost", + port: 6379, + } }); const redisBackend = createCacheBackend({ - type: 'redis', + type: "redis", redis: { client: redisClient, - keyPrefix: 'my-app:', + storeName: "my-app", }, }); // 2. Export your configured cache function export function cached( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ) { return baseCached(tool, { ...options, @@ -39,25 +41,25 @@ export function cached( // ===== Alternative: Multiple preset functions ===== export const redisCached = ( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ) => { return baseCached(tool, { ...options, store: redisBackend }); }; export const lruCached = ( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ) => { - const lruBackend = createCacheBackend({ type: 'lru', maxSize: 1000 }); + const lruBackend = createCacheBackend({ type: "lru", maxSize: 1000 }); return baseCached(tool, { ...options, store: lruBackend }); }; export const memoryCached = ( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ) => { - const memoryBackend = createCacheBackend({ type: 'memory', maxSize: 2000 }); + const memoryBackend = createCacheBackend({ type: "memory", maxSize: 2000 }); return baseCached(tool, { ...options, store: memoryBackend }); }; @@ -70,7 +72,7 @@ export const memoryCached = ( // In your tools files: // import { cached } from '@ai-sdk-tools/cache'; // import { getContext } from '@/ai/context'; -// +// // // Global tools (no context needed) // const weatherTool = cached(expensiveWeatherTool, { // ttl: 10 * 60 * 1000, // 10 minutes @@ -91,14 +93,14 @@ function createAppCacheBackend() { if (process.env.REDIS_URL) { const redis = Redis.createClient({ url: process.env.REDIS_URL }); return createCacheBackend({ - type: 'redis', - redis: { client: redis, keyPrefix: 'app:' }, + type: "redis", + redis: { client: redis, storeName: "app" }, }); } - + // Fallback to memory cache in development return createCacheBackend({ - type: 'memory', + type: "memory", maxSize: 1000, }); } @@ -106,8 +108,8 @@ function createAppCacheBackend() { const appCacheBackend = createAppCacheBackend(); export const appCached = ( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ) => { return baseCached(tool, { ...options, store: appCacheBackend }); }; diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts index 3f642ef..a07945b 100644 --- a/packages/cache/src/index.ts +++ b/packages/cache/src/index.ts @@ -1,15 +1,12 @@ /** * @ai-sdk-tools/cache - * - * Simple caching wrapper for AI SDK tools. Cache expensive tool executions + * + * Simple caching wrapper for AI SDK tools. Cache expensive tool executions * with zero configuration. */ -export { cached, createCached, cacheTools } from "./cache"; +export { cached, cacheTools, createCached, serializeValue } from "./cache"; export type { - CacheOptions, CachedTool, + CacheOptions, } from "./types"; - -// Re-export useful types from ai package -export type { Tool } from "ai"; diff --git a/packages/cache/src/types.ts b/packages/cache/src/types.ts index 22868e1..0e52412 100644 --- a/packages/cache/src/types.ts +++ b/packages/cache/src/types.ts @@ -6,28 +6,57 @@ import type { Tool } from "ai"; export interface CacheOptions { /** Cache duration in milliseconds (default: 5 minutes) */ ttl?: number; - + /** Maximum cache size (default: 1000 entries) */ maxSize?: number; - + /** Custom cache store backend */ store?: CacheStore; - + + /** + * Optional tool name to prefix cache keys with. + * Prevents collisions between tools using similar parameters. + */ + toolName?: string; + + /** + * Separator used between cache key components. + * + * @default ':' + */ + keySeparator?: string; + /** Custom cache key generator */ - keyGenerator?: (params: any, context?: any) => string; - - /** Function to generate cache key context */ + keyGenerator?: (options: { + params: any; + context?: any; + toolName?: string; + keySeparator?: string; + }) => string; + + /** + * Function to generate dynamic cache key context suffix. + * Appended to the end of the cache key with separator in between. + * Use for multi-tenant apps to isolate cache by user/team. + */ + cacheKeyContext?: () => string; + + /** + * @deprecated Use `cacheKeyContext` instead. + * + * Function to generate cache key context suffix. + */ cacheKey?: () => string; - + /** Whether to cache this result */ shouldCache?: (params: any, result: any) => boolean; - + /** Cache hit callback */ onHit?: (key: string) => void; - + /** Cache miss callback */ onMiss?: (key: string) => void; - + /** Enable debug logging */ debug?: boolean; } @@ -38,10 +67,10 @@ export interface CacheOptions { export interface CacheEntry { /** Cached result */ result: T; - + /** Timestamp when cached */ timestamp: number; - + /** Cache key */ key: string; } @@ -52,16 +81,16 @@ export interface CacheEntry { export interface CacheStats { /** Number of cache hits */ hits: number; - + /** Number of cache misses */ misses: number; - + /** Hit rate (0-1) */ hitRate: number; - + /** Current cache size */ size: number; - + /** Maximum cache size */ maxSize: number; } @@ -72,13 +101,13 @@ export interface CacheStats { export type CachedTool = T & { /** Get cache statistics */ getStats(): CacheStats; - + /** Clear cache entries */ clearCache(key?: string): void; - + /** Check if parameters are cached */ isCached(params: any): boolean | Promise; - + /** Get cache key for parameters */ getCacheKey(params: any): string; }; @@ -89,26 +118,28 @@ export type CachedTool = T & { */ export interface CacheStore { /** Get cached entry */ - get(key: string): CacheEntry | undefined | Promise | undefined>; - + get( + key: string, + ): CacheEntry | undefined | Promise | undefined>; + /** Set cache entry */ set(key: string, entry: CacheEntry): void | Promise; - + /** Delete cache entry */ delete(key: string): boolean | Promise; - + /** Clear all entries */ clear(): void | Promise; - + /** Check if key exists */ has(key: string): boolean | Promise; - + /** Get current size */ size(): number | Promise; - + /** Get all keys */ keys(): string[] | Promise; - + /** Get default TTL if configured */ getDefaultTTL?(): number | undefined; } From 520db197965a6b56e1d32eccdd51ea1643c29714 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 01:58:57 +0100 Subject: [PATCH 2/8] improve cacheTools types --- packages/cache/src/cache.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index f4cac9c..58ec742 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -645,7 +645,12 @@ export function cacheTools>( tools: TTools, options: CacheOptions = {}, ): { [K in keyof TTools]: CachedTool } { - return Object.fromEntries(Object.keys(tools).map((name) => ([[name as keyof TTools], cached(tools[name], options)]))) + const entries = Object.entries(tools).map(([name, tool]) => [ + name as keyof TTools, + cached(tool, options), + ] as const) + + return Object.fromEntries(entries) as { [K in keyof TTools]: CachedTool } } /** From 6005160155e069dc9619f1d30eb9060a557a426a Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 01:59:06 +0100 Subject: [PATCH 3/8] satisfy biome a bit --- packages/cache/src/examples/basic-usage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cache/src/examples/basic-usage.ts b/packages/cache/src/examples/basic-usage.ts index dc12f90..6b6815e 100644 --- a/packages/cache/src/examples/basic-usage.ts +++ b/packages/cache/src/examples/basic-usage.ts @@ -94,7 +94,7 @@ const calculatorTool = tool({ // Simple expression evaluation (use a proper math parser in production) const result = Function(`"use strict"; return (${expression})`)(); return { expression, result, success: true }; - } catch (error) { + } catch (_error) { return { expression, error: "Invalid expression", success: false }; } }, From ac0b6e52cbc4061802783bb34cb75e5fe79778e6 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 02:02:04 +0100 Subject: [PATCH 4/8] Update basic-usage.ts --- packages/cache/src/examples/basic-usage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cache/src/examples/basic-usage.ts b/packages/cache/src/examples/basic-usage.ts index 6b6815e..e22ecf0 100644 --- a/packages/cache/src/examples/basic-usage.ts +++ b/packages/cache/src/examples/basic-usage.ts @@ -199,7 +199,7 @@ export async function demonstrateCache() { export async function demonstrateCacheManagement() { console.log("šŸ”§ Cache Management Demo\n"); - const tool = cached(weatherTool, { debug: true }); + const tool = cached(expensiveWeatherTool, { debug: true }); // Add some entries await tool.execute?.({ location: "Paris", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); From a0f99ecc4a3c184fef86670b5f8bb431a77e5b1e Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 02:04:08 +0100 Subject: [PATCH 5/8] bring back removed comments --- packages/cache/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cache/README.md b/packages/cache/README.md index 1e4509b..3ade9ab 100644 --- a/packages/cache/README.md +++ b/packages/cache/README.md @@ -175,6 +175,7 @@ For multi-tenant apps, isolate cache by user/team using `cacheKeyContext`: ```typescript import { cached } from '@ai-sdk-tools/cache'; +// Your app's context system (could be React context, global state, etc.) const burnRateAnalysisTool = tool({ description: 'Analyze burn rate', @@ -183,7 +184,9 @@ const burnRateAnalysisTool = tool({ to: z.string(), }), execute: async ({ from, to }) => { + // Your app's way of getting current user/team context const currentUser = getCurrentUser(); + return await db.getBurnRate({ teamId: currentUser.teamId, from, @@ -192,6 +195,7 @@ const burnRateAnalysisTool = tool({ }, }); +// Cache with context - that's it! export const cachedBurnRateTool = cached(burnRateAnalysisTool, { cacheKeyContext: () => { const currentUser = getCurrentUser(); From ef7384f739224ad4447fbab0cf5586a2d80da24b Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 03:22:57 +0100 Subject: [PATCH 6/8] improve cacheTools types? --- packages/cache/src/cache.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 58ec742..198b871 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -1,7 +1,7 @@ import type { Tool } from "ai"; import { createCacheBackend } from "./backends"; import { LRUCacheStore } from "./cache-store"; -import {DEFAULT_CACHE_KEY_SEPARATOR, DEFAULT_STORE_NAME} from "./constants"; +import { DEFAULT_CACHE_KEY_SEPARATOR, DEFAULT_STORE_NAME } from "./constants"; import type { CachedTool, CacheOptions, CacheStats, CacheStore } from "./types"; /** @@ -641,16 +641,18 @@ export function createCachedFunction( /** * Cache multiple tools with the same configuration */ -export function cacheTools>( - tools: TTools, - options: CacheOptions = {}, -): { [K in keyof TTools]: CachedTool } { - const entries = Object.entries(tools).map(([name, tool]) => [ - name as keyof TTools, - cached(tool, options), - ] as const) - - return Object.fromEntries(entries) as { [K in keyof TTools]: CachedTool } +export function cacheTools< + T extends Tool, + TTools extends Record, +>(tools: TTools, options: CacheOptions = {}) { + const entries = Object.entries(tools); + const cachedEntries = entries.map( + ([name, tool]) => [name, cached(tool, options)] as const, + ) as { [K in keyof TTools]: [K, CachedTool] }[keyof TTools][]; + + return Object.fromEntries(cachedEntries) as { + [K in (typeof cachedEntries)[number] as K[0]]: K[1]; + }; } /** From 23101fa386a6d4fdc9bde020a670ddf4b649beb2 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 03:24:35 +0100 Subject: [PATCH 7/8] Update cache.ts --- packages/cache/src/cache.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 198b871..3b1d8d4 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -641,17 +641,16 @@ export function createCachedFunction( /** * Cache multiple tools with the same configuration */ -export function cacheTools< - T extends Tool, - TTools extends Record, ->(tools: TTools, options: CacheOptions = {}) { - const entries = Object.entries(tools); - const cachedEntries = entries.map( - ([name, tool]) => [name, cached(tool, options)] as const, - ) as { [K in keyof TTools]: [K, CachedTool] }[keyof TTools][]; - - return Object.fromEntries(cachedEntries) as { - [K in (typeof cachedEntries)[number] as K[0]]: K[1]; +export function cacheTools>( + tools: TTools, + options: CacheOptions = {}, +): { [K in keyof TTools]: CachedTool } { + const entries = Object.entries(tools).map( + ([name, tool]) => [name as keyof TTools, cached(tool, options)] as const, + ); + + return Object.fromEntries(entries) as { + [K in keyof TTools]: CachedTool; }; } From 950d4d18b90583a7d408f8141cb956cf33d56f54 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 6 Nov 2025 03:37:00 +0100 Subject: [PATCH 8/8] biome format --- packages/cache/src/backends/redis.ts | 12 ++-- packages/cache/src/examples/basic-usage.ts | 74 ++++++++++++++++------ packages/cache/src/examples/user-config.ts | 2 +- packages/cache/tsconfig.json | 11 +--- 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/packages/cache/src/backends/redis.ts b/packages/cache/src/backends/redis.ts index f980614..53fc448 100644 --- a/packages/cache/src/backends/redis.ts +++ b/packages/cache/src/backends/redis.ts @@ -1,7 +1,4 @@ -import { - DEFAULT_CACHE_KEY_SEPARATOR, - DEFAULT_STORE_NAME, -} from "../constants"; +import { DEFAULT_CACHE_KEY_SEPARATOR, DEFAULT_STORE_NAME } from "../constants"; import type { CacheEntry, CacheStore } from "../types"; /** @@ -16,9 +13,10 @@ export class RedisCacheStore implements CacheStore { this.redis = redisClient; // Append separator if storeName doesn't end with a common separator const endsWithSeparator = /[:|\-_]$/.test(storeName); - this.keyPrefix = storeName && !endsWithSeparator - ? `${storeName}${DEFAULT_CACHE_KEY_SEPARATOR}` - : storeName; + this.keyPrefix = + storeName && !endsWithSeparator + ? `${storeName}${DEFAULT_CACHE_KEY_SEPARATOR}` + : storeName; } private getKey(key: string): string { diff --git a/packages/cache/src/examples/basic-usage.ts b/packages/cache/src/examples/basic-usage.ts index e22ecf0..55cdaa9 100644 --- a/packages/cache/src/examples/basic-usage.ts +++ b/packages/cache/src/examples/basic-usage.ts @@ -146,15 +146,24 @@ export async function demonstrateCache() { console.log("=== Weather Tool Demo ==="); console.log("First call (should be slow):"); - const weather1 = await weatherTool.execute?.({ location: "New York", units: 'fahrenheit' }, {toolCallId: 'weatherTool', messages: []}); + const weather1 = await weatherTool.execute?.( + { location: "New York", units: "fahrenheit" }, + { toolCallId: "weatherTool", messages: [] }, + ); console.log("Result:", weather1); console.log("\nSecond call with same params (should be fast):"); - const weather2 = await weatherTool.execute?.({ location: "New York", units: 'fahrenheit' }, {toolCallId: 'weatherTool', messages: []}); + const weather2 = await weatherTool.execute?.( + { location: "New York", units: "fahrenheit" }, + { toolCallId: "weatherTool", messages: [] }, + ); console.log("Result:", weather2); console.log("\nThird call with different params (should be slow):"); - const weather3 = await weatherTool.execute?.({ location: "London", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); + const weather3 = await weatherTool.execute?.( + { location: "London", units: "celsius" }, + { toolCallId: "weatherTool", messages: [] }, + ); console.log("Result:", weather3); // Show cache stats @@ -163,17 +172,23 @@ export async function demonstrateCache() { // Test burn rate tool console.log("\n=== Burn Rate Analysis Demo ==="); - const analysis1 = await cachedBurnRateTool.execute?.({ - companyId: "company-123", - months: 12, - }, {toolCallId: 'cachedBurnRateTool', messages: []}); + const analysis1 = await cachedBurnRateTool.execute?.( + { + companyId: "company-123", + months: 12, + }, + { toolCallId: "cachedBurnRateTool", messages: [] }, + ); console.log("Analysis result:", analysis1); // Same params - should hit cache - const analysis2 = await cachedBurnRateTool.execute?.({ - companyId: "company-123", - months: 12, - }, {toolCallId: 'cachedBurnRateTool', messages: []}); + const analysis2 = await cachedBurnRateTool.execute?.( + { + companyId: "company-123", + months: 12, + }, + { toolCallId: "cachedBurnRateTool", messages: [] }, + ); console.log("Cached analysis:", analysis2); console.log("\nBurn rate tool cache stats:", cachedBurnRateTool.getStats()); @@ -181,11 +196,23 @@ export async function demonstrateCache() { // Test multiple tools console.log("\n=== Multiple Tools Demo ==="); - await calculator.execute?.({ expression: "15 * 8" }, {toolCallId: 'calculator', messages: []}); - await calculator.execute?.({ expression: "15 * 8" }, {toolCallId: 'calculator', messages: []}); // Should hit cache - - await database.execute?.({ query: "SELECT * FROM users", table: "users" }, {toolCallId: 'database', messages: []}); - await database.execute?.({ query: "SELECT * FROM users", table: "users" }, {toolCallId: 'database', messages: []}); // Should hit cache + await calculator.execute?.( + { expression: "15 * 8" }, + { toolCallId: "calculator", messages: [] }, + ); + await calculator.execute?.( + { expression: "15 * 8" }, + { toolCallId: "calculator", messages: [] }, + ); // Should hit cache + + await database.execute?.( + { query: "SELECT * FROM users", table: "users" }, + { toolCallId: "database", messages: [] }, + ); + await database.execute?.( + { query: "SELECT * FROM users", table: "users" }, + { toolCallId: "database", messages: [] }, + ); // Should hit cache console.log("\nCalculator cache stats:", calculator.getStats()); console.log("Database cache stats:", database.getStats()); @@ -202,9 +229,18 @@ export async function demonstrateCacheManagement() { const tool = cached(expensiveWeatherTool, { debug: true }); // Add some entries - await tool.execute?.({ location: "Paris", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); - await tool.execute?.({ location: "Tokyo", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); - await tool.execute?.({ location: "Sydney", units: 'celsius' }, {toolCallId: 'weatherTool', messages: []}); + await tool.execute?.( + { location: "Paris", units: "celsius" }, + { toolCallId: "weatherTool", messages: [] }, + ); + await tool.execute?.( + { location: "Tokyo", units: "celsius" }, + { toolCallId: "weatherTool", messages: [] }, + ); + await tool.execute?.( + { location: "Sydney", units: "celsius" }, + { toolCallId: "weatherTool", messages: [] }, + ); console.log("Cache stats after adding entries:", tool.getStats()); diff --git a/packages/cache/src/examples/user-config.ts b/packages/cache/src/examples/user-config.ts index 0cdaeff..70bcbe5 100644 --- a/packages/cache/src/examples/user-config.ts +++ b/packages/cache/src/examples/user-config.ts @@ -16,7 +16,7 @@ const redisClient = Redis.createClient({ socket: { host: "localhost", port: 6379, - } + }, }); const redisBackend = createCacheBackend({ diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json index 264eed8..5d96acd 100644 --- a/packages/cache/tsconfig.json +++ b/packages/cache/tsconfig.json @@ -18,13 +18,6 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, - "include": [ - "src/**/*" - ], - "exclude": [ - "dist", - "node_modules", - "**/*.test.ts", - "**/*.spec.ts" - ] + "include": ["src/**/*"], + "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.spec.ts"] }