diff --git a/.changeset/eight-laws-listen.md b/.changeset/eight-laws-listen.md new file mode 100644 index 000000000..dc6866444 --- /dev/null +++ b/.changeset/eight-laws-listen.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": minor +--- + +Remove old act cache diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 44b6c87aa..da99977be 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -542,7 +542,6 @@ export class StagehandPage { requestId, variables, previousSelectors: [], - skipActionCacheForThisStep: false, domSettleTimeoutMs, timeoutMs, }) @@ -670,10 +669,6 @@ export class StagehandPage { }, }); - if (this.stagehand.enableCaching) { - this.stagehand.llmProvider.cleanRequestCache(requestId); - } - throw e; }); } @@ -796,10 +791,6 @@ export class StagehandPage { }, }); - if (this.stagehand.enableCaching) { - this.stagehand.llmProvider.cleanRequestCache(requestId); - } - throw e; }); } diff --git a/lib/cache.ts b/lib/cache.ts deleted file mode 100644 index a9e2a981d..000000000 --- a/lib/cache.ts +++ /dev/null @@ -1,98 +0,0 @@ -import fs from "fs"; -const observationsPath = "./.cache/observations.json"; -const actionsPath = "./.cache/actions.json"; - -/** - * A file system cache to skip inference when repeating steps - * It also acts as the source of truth for identifying previously seen actions and observations - */ -class Cache { - disabled: boolean; - - constructor({ disabled = false } = {}) { - this.disabled = disabled; - if (!this.disabled) { - this.initCache(); - } - } - - readObservations() { - if (this.disabled) { - return {}; - } - try { - return JSON.parse(fs.readFileSync(observationsPath, "utf8")); - } catch (error) { - console.error("Error reading from observations.json", error); - return {}; - } - } - - readActions() { - if (this.disabled) { - return {}; - } - try { - return JSON.parse(fs.readFileSync(actionsPath, "utf8")); - } catch (error) { - console.error("Error reading from actions.json", error); - return {}; - } - } - - writeObservations({ - key, - value, - }: { - key: string; - value: { id: string; result: string }; - }) { - if (this.disabled) { - return; - } - - const observations = this.readObservations(); - observations[key] = value; - fs.writeFileSync(observationsPath, JSON.stringify(observations, null, 2)); - } - - writeActions({ - key, - value, - }: { - key: string; - value: { id: string; result: string }; - }) { - if (this.disabled) { - return; - } - - const actions = this.readActions(); - actions[key] = value; - fs.writeFileSync(actionsPath, JSON.stringify(actions, null, 2)); - } - - evictCache() { - throw new Error("implement me"); - } - - private initCache() { - if (this.disabled) { - return; - } - const cacheDir = ".cache"; - - if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir); - } - if (!fs.existsSync(actionsPath)) { - fs.writeFileSync(actionsPath, JSON.stringify({})); - } - - if (!fs.existsSync(observationsPath)) { - fs.writeFileSync(observationsPath, JSON.stringify({})); - } - } -} - -export default Cache; diff --git a/lib/cache/ActionCache.ts b/lib/cache/ActionCache.ts deleted file mode 100644 index b54801d6c..000000000 --- a/lib/cache/ActionCache.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { LogLine } from "../../types/log"; -import { BaseCache, CacheEntry } from "./BaseCache"; - -export interface PlaywrightCommand { - method: string; - args: string[]; -} - -export interface ActionEntry extends CacheEntry { - data: { - playwrightCommand: PlaywrightCommand; - componentString: string; - xpaths: string[]; - newStepString: string; - completed: boolean; - previousSelectors: string[]; - action: string; - }; -} - -/** - * ActionCache handles logging and retrieving actions along with their Playwright commands. - */ -export class ActionCache extends BaseCache { - constructor( - logger: (message: LogLine) => void, - cacheDir?: string, - cacheFile?: string, - ) { - super(logger, cacheDir, cacheFile || "action_cache.json"); - } - - public async addActionStep({ - url, - action, - previousSelectors, - playwrightCommand, - componentString, - xpaths, - newStepString, - completed, - requestId, - }: { - url: string; - action: string; - previousSelectors: string[]; - playwrightCommand: PlaywrightCommand; - componentString: string; - requestId: string; - xpaths: string[]; - newStepString: string; - completed: boolean; - }): Promise { - this.logger({ - category: "action_cache", - message: "adding action step to cache", - level: 1, - auxiliary: { - action: { - value: action, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - url: { - value: url, - type: "string", - }, - previousSelectors: { - value: JSON.stringify(previousSelectors), - type: "object", - }, - }, - }); - - await this.set( - { url, action, previousSelectors }, - { - playwrightCommand, - componentString, - xpaths, - newStepString, - completed, - previousSelectors, - action, - }, - requestId, - ); - } - - /** - * Retrieves all actions for a specific trajectory. - * @param trajectoryId - Unique identifier for the trajectory. - * @param requestId - The identifier for the current request. - * @returns An array of TrajectoryEntry objects or null if not found. - */ - public async getActionStep({ - url, - action, - previousSelectors, - requestId, - }: { - url: string; - action: string; - previousSelectors: string[]; - requestId: string; - }): Promise { - const data = await super.get({ url, action, previousSelectors }, requestId); - if (!data) { - return null; - } - - return data; - } - - public async removeActionStep(cacheHashObj: { - url: string; - action: string; - previousSelectors: string[]; - requestId: string; - }): Promise { - await super.delete(cacheHashObj); - } - - /** - * Clears all actions for a specific trajectory. - * @param trajectoryId - Unique identifier for the trajectory. - * @param requestId - The identifier for the current request. - */ - public async clearAction(requestId: string): Promise { - await super.deleteCacheForRequestId(requestId); - this.logger({ - category: "action_cache", - message: "cleared action for ID", - level: 1, - auxiliary: { - requestId: { - value: requestId, - type: "string", - }, - }, - }); - } - - /** - * Resets the entire action cache. - */ - public async resetCache(): Promise { - await super.resetCache(); - this.logger({ - category: "action_cache", - message: "Action cache has been reset.", - level: 1, - }); - } -} diff --git a/lib/cache/BaseCache.ts b/lib/cache/BaseCache.ts deleted file mode 100644 index 18ef61690..000000000 --- a/lib/cache/BaseCache.ts +++ /dev/null @@ -1,568 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as crypto from "crypto"; -import { LogLine } from "../../types/log"; - -export interface CacheEntry { - timestamp: number; - data: unknown; - requestId: string; -} - -export interface CacheStore { - [key: string]: CacheEntry; -} - -export class BaseCache { - private readonly CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 1 week in milliseconds - private readonly CLEANUP_PROBABILITY = 0.01; // 1% chance - - protected cacheDir: string; - protected cacheFile: string; - protected lockFile: string; - protected logger: (message: LogLine) => void; - - private readonly LOCK_TIMEOUT_MS = 1_000; - protected lockAcquired = false; - protected lockAcquireFailures = 0; - - // Added for request ID tracking - protected requestIdToUsedHashes: { [key: string]: string[] } = {}; - - constructor( - logger: (message: LogLine) => void, - cacheDir: string = path.join(process.cwd(), "tmp", ".cache"), - cacheFile: string = "cache.json", - ) { - this.logger = logger; - this.cacheDir = cacheDir; - this.cacheFile = path.join(cacheDir, cacheFile); - this.lockFile = path.join(cacheDir, "cache.lock"); - this.ensureCacheDirectory(); - this.setupProcessHandlers(); - } - - private setupProcessHandlers(): void { - const releaseLockAndExit = () => { - this.releaseLock(); - process.exit(); - }; - - process.on("exit", releaseLockAndExit); - process.on("SIGINT", releaseLockAndExit); - process.on("SIGTERM", releaseLockAndExit); - process.on("uncaughtException", (err) => { - this.logger({ - category: "base_cache", - message: "uncaught exception", - level: 2, - auxiliary: { - error: { - value: err.message, - type: "string", - }, - trace: { - value: err.stack, - type: "string", - }, - }, - }); - if (this.lockAcquired) { - releaseLockAndExit(); - } - }); - } - - protected ensureCacheDirectory(): void { - if (!fs.existsSync(this.cacheDir)) { - fs.mkdirSync(this.cacheDir, { recursive: true }); - this.logger({ - category: "base_cache", - message: "created cache directory", - level: 1, - auxiliary: { - cacheDir: { - value: this.cacheDir, - type: "string", - }, - }, - }); - } - } - - protected createHash(data: unknown): string { - const hash = crypto.createHash("sha256"); - return hash.update(JSON.stringify(data)).digest("hex"); - } - - protected sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - public async acquireLock(): Promise { - const startTime = Date.now(); - while (Date.now() - startTime < this.LOCK_TIMEOUT_MS) { - try { - if (fs.existsSync(this.lockFile)) { - const lockAge = Date.now() - fs.statSync(this.lockFile).mtimeMs; - if (lockAge > this.LOCK_TIMEOUT_MS) { - fs.unlinkSync(this.lockFile); - this.logger({ - category: "base_cache", - message: "Stale lock file removed", - level: 1, - }); - } - } - - fs.writeFileSync(this.lockFile, process.pid.toString(), { flag: "wx" }); - this.lockAcquireFailures = 0; - this.lockAcquired = true; - this.logger({ - category: "base_cache", - message: "Lock acquired", - level: 1, - }); - return true; - } catch (e) { - this.logger({ - category: "base_cache", - message: "error acquiring lock", - level: 2, - auxiliary: { - trace: { - value: e.stack, - type: "string", - }, - message: { - value: e.message, - type: "string", - }, - }, - }); - await this.sleep(5); - } - } - this.logger({ - category: "base_cache", - message: "Failed to acquire lock after timeout", - level: 2, - }); - this.lockAcquireFailures++; - if (this.lockAcquireFailures >= 3) { - this.logger({ - category: "base_cache", - message: - "Failed to acquire lock 3 times in a row. Releasing lock manually.", - level: 1, - }); - this.releaseLock(); - } - return false; - } - - public releaseLock(): void { - try { - if (fs.existsSync(this.lockFile)) { - fs.unlinkSync(this.lockFile); - this.logger({ - category: "base_cache", - message: "Lock released", - level: 1, - }); - } - this.lockAcquired = false; - } catch (error) { - this.logger({ - category: "base_cache", - message: "error releasing lock", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - } - } - - /** - * Cleans up stale cache entries that exceed the maximum age. - */ - public async cleanupStaleEntries(): Promise { - if (!(await this.acquireLock())) { - this.logger({ - category: "llm_cache", - message: "failed to acquire lock for cleanup", - level: 2, - }); - return; - } - - try { - const cache = this.readCache(); - const now = Date.now(); - let entriesRemoved = 0; - - for (const [hash, entry] of Object.entries(cache)) { - if (now - entry.timestamp > this.CACHE_MAX_AGE_MS) { - delete cache[hash]; - entriesRemoved++; - } - } - - if (entriesRemoved > 0) { - this.writeCache(cache); - this.logger({ - category: "llm_cache", - message: "cleaned up stale cache entries", - level: 1, - auxiliary: { - entriesRemoved: { - value: entriesRemoved.toString(), - type: "integer", - }, - }, - }); - } - } catch (error) { - this.logger({ - category: "llm_cache", - message: "error during cache cleanup", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - } finally { - this.releaseLock(); - } - } - - protected readCache(): CacheStore { - if (fs.existsSync(this.cacheFile)) { - try { - const data = fs.readFileSync(this.cacheFile, "utf-8"); - return JSON.parse(data) as CacheStore; - } catch (error) { - this.logger({ - category: "base_cache", - message: "error reading cache file. resetting cache.", - level: 1, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - this.resetCache(); - return {}; - } - } - return {}; - } - - protected writeCache(cache: CacheStore): void { - try { - fs.writeFileSync(this.cacheFile, JSON.stringify(cache, null, 2)); - this.logger({ - category: "base_cache", - message: "Cache written to file", - level: 1, - }); - } catch (error) { - this.logger({ - category: "base_cache", - message: "error writing cache file", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - } finally { - this.releaseLock(); - } - } - - /** - * Retrieves data from the cache based on the provided options. - * @param hashObj - The options used to generate the cache key. - * @param requestId - The identifier for the current request. - * @returns The cached data if available, otherwise null. - */ - public async get( - hashObj: Record | string, - requestId: string, - ): Promise { - if (!(await this.acquireLock())) { - this.logger({ - category: "base_cache", - message: "Failed to acquire lock for getting cache", - level: 2, - }); - return null; - } - - try { - const hash = this.createHash(hashObj); - const cache = this.readCache(); - - if (cache[hash]) { - this.trackRequestIdUsage(requestId, hash); - return cache[hash].data; - } - return null; - } catch (error) { - this.logger({ - category: "base_cache", - message: "error getting cache. resetting cache.", - level: 1, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - - this.resetCache(); - return null; - } finally { - this.releaseLock(); - } - } - - /** - * Stores data in the cache based on the provided options and requestId. - * @param hashObj - The options used to generate the cache key. - * @param data - The data to be cached. - * @param requestId - The identifier for the cache entry. - */ - public async set( - hashObj: Record, - data: T["data"], - requestId: string, - ): Promise { - if (!(await this.acquireLock())) { - this.logger({ - category: "base_cache", - message: "Failed to acquire lock for setting cache", - level: 2, - }); - return; - } - - try { - const hash = this.createHash(hashObj); - const cache = this.readCache(); - cache[hash] = { - data, - timestamp: Date.now(), - requestId, - }; - - this.writeCache(cache); - this.trackRequestIdUsage(requestId, hash); - } catch (error) { - this.logger({ - category: "base_cache", - message: "error setting cache. resetting cache.", - level: 1, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - - this.resetCache(); - } finally { - this.releaseLock(); - - if (Math.random() < this.CLEANUP_PROBABILITY) { - this.cleanupStaleEntries(); - } - } - } - - public async delete(hashObj: Record): Promise { - if (!(await this.acquireLock())) { - this.logger({ - category: "base_cache", - message: "Failed to acquire lock for removing cache entry", - level: 2, - }); - return; - } - - try { - const hash = this.createHash(hashObj); - const cache = this.readCache(); - - if (cache[hash]) { - delete cache[hash]; - this.writeCache(cache); - } else { - this.logger({ - category: "base_cache", - message: "Cache entry not found to delete", - level: 1, - }); - } - } catch (error) { - this.logger({ - category: "base_cache", - message: "error removing cache entry", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - } finally { - this.releaseLock(); - } - } - - /** - * Tracks the usage of a hash with a specific requestId. - * @param requestId - The identifier for the current request. - * @param hash - The cache key hash. - */ - protected trackRequestIdUsage(requestId: string, hash: string): void { - this.requestIdToUsedHashes[requestId] ??= []; - this.requestIdToUsedHashes[requestId].push(hash); - } - - /** - * Deletes all cache entries associated with a specific requestId. - * @param requestId - The identifier for the request whose cache entries should be deleted. - */ - public async deleteCacheForRequestId(requestId: string): Promise { - if (!(await this.acquireLock())) { - this.logger({ - category: "base_cache", - message: "Failed to acquire lock for deleting cache", - level: 2, - }); - return; - } - try { - const cache = this.readCache(); - const hashes = this.requestIdToUsedHashes[requestId] ?? []; - let entriesRemoved = 0; - for (const hash of hashes) { - if (cache[hash]) { - delete cache[hash]; - entriesRemoved++; - } - } - if (entriesRemoved > 0) { - this.writeCache(cache); - } else { - this.logger({ - category: "base_cache", - message: "no cache entries found for requestId", - level: 1, - auxiliary: { - requestId: { - value: requestId, - type: "string", - }, - }, - }); - } - // Remove the requestId from the mapping after deletion - delete this.requestIdToUsedHashes[requestId]; - } catch (error) { - this.logger({ - category: "base_cache", - message: "error deleting cache for requestId", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - }, - }); - } finally { - this.releaseLock(); - } - } - - /** - * Resets the entire cache by clearing the cache file. - */ - public resetCache(): void { - try { - fs.writeFileSync(this.cacheFile, "{}"); - this.requestIdToUsedHashes = {}; // Reset requestId tracking - } catch (error) { - this.logger({ - category: "base_cache", - message: "error resetting cache", - level: 2, - auxiliary: { - error: { - value: error.message, - type: "string", - }, - trace: { - value: error.stack, - type: "string", - }, - }, - }); - } finally { - this.releaseLock(); - } - } -} diff --git a/lib/cache/LLMCache.ts b/lib/cache/LLMCache.ts deleted file mode 100644 index 53bd78213..000000000 --- a/lib/cache/LLMCache.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BaseCache, CacheEntry } from "./BaseCache"; - -export class LLMCache extends BaseCache { - constructor( - logger: (message: { - category?: string; - message: string; - level?: number; - }) => void, - cacheDir?: string, - cacheFile?: string, - ) { - super(logger, cacheDir, cacheFile || "llm_calls.json"); - } - - /** - * Overrides the get method to track used hashes by requestId. - * @param options - The options used to generate the cache key. - * @param requestId - The identifier for the current request. - * @returns The cached data if available, otherwise null. - */ - public async get( - options: Record, - requestId: string, - ): Promise { - const data = await super.get(options, requestId); - return data as T | null; // TODO: remove this cast - } - - /** - * Overrides the set method to include cache cleanup logic. - * @param options - The options used to generate the cache key. - * @param data - The data to be cached. - * @param requestId - The identifier for the current request. - */ - public async set( - options: Record, - data: unknown, - requestId: string, - ): Promise { - await super.set(options, data, requestId); - this.logger({ - category: "llm_cache", - message: "Cache miss - saved new response", - level: 1, - }); - } -} diff --git a/lib/dom/candidateCollector.ts b/lib/dom/candidateCollector.ts index e577653f1..5bb250231 100644 --- a/lib/dom/candidateCollector.ts +++ b/lib/dom/candidateCollector.ts @@ -9,8 +9,6 @@ import { } from "./elementCheckUtils"; import { generateXPathsForElement as generateXPaths } from "./xpathUtils"; -const xpathCache: Map = new Map(); - /** * `collectCandidateElements` performs a depth-first traversal (despite the BFS naming) of the given `candidateContainerRoot` * to find “candidate elements” or text nodes that meet certain criteria (e.g., visible, active, @@ -66,11 +64,7 @@ export async function collectCandidateElements( const xpathLists = await Promise.all( candidateElements.map((elem) => { - if (xpathCache.has(elem)) { - return Promise.resolve(xpathCache.get(elem)!); - } return generateXPaths(elem).then((xpaths: string[]) => { - xpathCache.set(elem, xpaths); return xpaths; }); }), diff --git a/lib/handlers/actHandler.ts b/lib/handlers/actHandler.ts index c6da68138..69899db2a 100644 --- a/lib/handlers/actHandler.ts +++ b/lib/handlers/actHandler.ts @@ -1,10 +1,9 @@ -import { Locator, Page } from "@playwright/test"; +import { Locator } from "@playwright/test"; import { LogLine } from "../../types/log"; import { PlaywrightCommandException, PlaywrightCommandMethodNotSupportedException, } from "../../types/playwright"; -import { ActionCache } from "../cache/ActionCache"; import { act, fillInVariables, verifyActCompletion } from "../inference"; import { LLMClient } from "../llm/LLMClient"; import { LLMProvider } from "../llm/LLMProvider"; @@ -35,9 +34,7 @@ export class StagehandActHandler { private readonly stagehandPage: StagehandPage; private readonly verbose: 0 | 1 | 2; private readonly llmProvider: LLMProvider; - private readonly enableCaching: boolean; private readonly logger: (logLine: LogLine) => void; - private readonly actionCache: ActionCache | undefined; private readonly actions: { [key: string]: { result: string; action: string }; }; @@ -49,7 +46,6 @@ export class StagehandActHandler { stagehand, verbose, llmProvider, - enableCaching, logger, stagehandPage, userProvidedInstructions, @@ -71,9 +67,7 @@ export class StagehandActHandler { this.stagehand = stagehand; this.verbose = verbose; this.llmProvider = llmProvider; - this.enableCaching = enableCaching; this.logger = logger; - this.actionCache = enableCaching ? new ActionCache(this.logger) : undefined; this.actions = {}; this.stagehandPage = stagehandPage; this.userProvidedInstructions = userProvidedInstructions; @@ -456,139 +450,6 @@ export class StagehandActHandler { } } - private async _getComponentString(locator: Locator) { - return await locator.evaluate((el) => { - // Create a clone of the element to avoid modifying the original - const clone = el.cloneNode(true) as HTMLElement; - - // Keep only specific stable attributes that help identify elements - const attributesToKeep = [ - "type", - "name", - "placeholder", - "aria-label", - "role", - "href", - "title", - "alt", - ]; - - // Remove all attributes except those we want to keep - Array.from(clone.attributes).forEach((attr) => { - if (!attributesToKeep.includes(attr.name)) { - clone.removeAttribute(attr.name); - } - }); - - const outerHtml = clone.outerHTML; - return outerHtml.trim().replace(/\s+/g, " "); - }); - } - - private async handlePossiblePageNavigation( - actionDescription: string, - xpath: string, - initialUrl: string, - domSettleTimeoutMs: number, - ): Promise { - // 1) Log that we’re about to check for page navigation - this.logger({ - category: "action", - message: `${actionDescription}, checking for page navigation`, - level: 1, - auxiliary: { - xpath: { - value: xpath, - type: "string", - }, - }, - }); - - // 2) Race against a new page opening in a tab or timing out - const newOpenedTab = await Promise.race([ - new Promise((resolve) => { - // TODO: This is a hack to get the new page. - // We should find a better way to do this. - this.stagehandPage.context.once("page", (page) => resolve(page)); - setTimeout(() => resolve(null), 1_500); - }), - ]); - - // 3) Log whether a new tab was opened - this.logger({ - category: "action", - message: `${actionDescription} complete`, - level: 1, - auxiliary: { - newOpenedTab: { - value: newOpenedTab ? "opened a new tab" : "no new tabs opened", - type: "string", - }, - }, - }); - - // 4) If new page opened in new tab, close the tab, then navigate our main page - if (newOpenedTab) { - this.logger({ - category: "action", - message: "new page detected (new tab) with URL", - level: 1, - auxiliary: { - url: { - value: newOpenedTab.url(), - type: "string", - }, - }, - }); - await newOpenedTab.close(); - await this.stagehandPage.page.goto(newOpenedTab.url()); - await this.stagehandPage.page.waitForLoadState("domcontentloaded"); - } - - // 5) Wait for the DOM to settle - await this.stagehandPage - ._waitForSettledDom(domSettleTimeoutMs) - .catch((e) => { - this.logger({ - category: "action", - message: "wait for settled DOM timeout hit", - level: 1, - auxiliary: { - trace: { - value: e.stack, - type: "string", - }, - message: { - value: e.message, - type: "string", - }, - }, - }); - }); - - // 6) Log that we finished waiting for possible navigation - this.logger({ - category: "action", - message: "finished waiting for (possible) page navigation", - level: 1, - }); - - // 7) If URL changed from initial, log the new URL - if (this.stagehandPage.page.url() !== initialUrl) { - this.logger({ - category: "action", - message: "new page detected with URL", - level: 1, - auxiliary: { - url: { - value: this.stagehandPage.page.url(), - type: "string", - }, - }, - }); - } - } - public async act({ action, steps = "", @@ -598,7 +459,6 @@ export class StagehandActHandler { requestId, variables, previousSelectors, - skipActionCacheForThisStep = false, domSettleTimeoutMs, timeoutMs, startTime = Date.now(), @@ -611,7 +471,6 @@ export class StagehandActHandler { requestId?: string; variables: Record; previousSelectors: string[]; - skipActionCacheForThisStep: boolean; domSettleTimeoutMs?: number; timeoutMs?: number; startTime?: number; @@ -744,17 +603,11 @@ export class StagehandActHandler { requestId, variables, previousSelectors, - skipActionCacheForThisStep, domSettleTimeoutMs, timeoutMs, startTime, }); } else { - if (this.enableCaching) { - this.llmProvider.cleanRequestCache(requestId); - this.actionCache?.deleteCacheForRequestId(requestId); - } - return { success: false, message: `Action was not able to be completed.`, @@ -842,8 +695,6 @@ export class StagehandActHandler { throw new Error("None of the provided XPaths could be located."); } - const originalUrl = this.stagehandPage.page.url(); - const componentString = await this._getComponentString(locator); const responseArgs = [...args]; if (variables) { @@ -870,41 +721,6 @@ export class StagehandActHandler { steps += newStepString; - if (this.enableCaching) { - this.actionCache - .addActionStep({ - action, - url: originalUrl, - previousSelectors, - playwrightCommand: { - method, - args: responseArgs.map((arg) => arg?.toString() || ""), - }, - componentString, - requestId, - xpaths, - newStepString, - completed: response.completed, - }) - .catch((e) => { - this.logger({ - category: "action", - message: "error adding action step to cache", - level: 1, - auxiliary: { - error: { - value: e.message, - type: "string", - }, - trace: { - value: e.stack, - type: "string", - }, - }, - }); - }); - } - if (this.stagehandPage.page.url() !== initialUrl) { steps += ` Result (Important): Page URL changed from ${initialUrl} to ${this.stagehandPage.page.url()}\n\n`; @@ -960,7 +776,6 @@ export class StagehandActHandler { requestId, variables, previousSelectors: [...previousSelectors, foundXpath], - skipActionCacheForThisStep: false, domSettleTimeoutMs, timeoutMs, startTime, @@ -1009,7 +824,6 @@ export class StagehandActHandler { requestId, variables, previousSelectors, - skipActionCacheForThisStep, domSettleTimeoutMs, timeoutMs, startTime, @@ -1017,10 +831,6 @@ export class StagehandActHandler { } await this._recordAction(action, ""); - if (this.enableCaching) { - this.llmProvider.cleanRequestCache(requestId); - this.actionCache.deleteCacheForRequestId(requestId); - } return { success: false, @@ -1045,11 +855,6 @@ export class StagehandActHandler { }, }); - if (this.enableCaching) { - this.llmProvider.cleanRequestCache(requestId); - this.actionCache.deleteCacheForRequestId(requestId); - } - return { success: false, message: `Error performing action - C: ${error.message}`, diff --git a/lib/index.ts b/lib/index.ts index b3f16337f..af5820721 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -487,8 +487,7 @@ export class Stagehand { this.enableCaching = enableCaching ?? (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true"); - this.llmProvider = - llmProvider || new LLMProvider(this.logger, this.enableCaching); + this.llmProvider = llmProvider || new LLMProvider(this.logger); this.intEnv = env; this.apiKey = apiKey ?? process.env.BROWSERBASE_API_KEY; this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID; diff --git a/lib/llm/AnthropicClient.ts b/lib/llm/AnthropicClient.ts index a7d97316d..7ecdac3dc 100644 --- a/lib/llm/AnthropicClient.ts +++ b/lib/llm/AnthropicClient.ts @@ -8,7 +8,6 @@ import { import { zodToJsonSchema } from "zod-to-json-schema"; import { LogLine } from "../../types/log"; import { AnthropicJsonSchemaObject, AvailableModel } from "../../types/model"; -import { LLMCache } from "../cache/LLMCache"; import { CreateChatCompletionOptions, LLMClient, @@ -18,28 +17,20 @@ import { export class AnthropicClient extends LLMClient { public type = "anthropic" as const; private client: Anthropic; - private cache: LLMCache | undefined; - private enableCaching: boolean; public clientOptions: ClientOptions; constructor({ - enableCaching = false, - cache, modelName, clientOptions, userProvidedInstructions, }: { logger: (message: LogLine) => void; - enableCaching?: boolean; - cache?: LLMCache; modelName: AvailableModel; clientOptions?: ClientOptions; userProvidedInstructions?: string; }) { super(modelName); this.client = new Anthropic(clientOptions); - this.cache = cache; - this.enableCaching = enableCaching; this.modelName = modelName; this.clientOptions = clientOptions; this.userProvidedInstructions = userProvidedInstructions; @@ -65,62 +56,6 @@ export class AnthropicClient extends LLMClient { }, }); - // Try to get cached response - const cacheOptions = { - model: this.modelName, - messages: options.messages, - temperature: options.temperature, - image: options.image, - response_model: options.response_model, - tools: options.tools, - retries: retries, - }; - - if (this.enableCaching) { - const cachedResponse = await this.cache.get( - cacheOptions, - options.requestId, - ); - if (cachedResponse) { - logger({ - category: "llm_cache", - message: "LLM cache hit - returning cached response", - level: 1, - auxiliary: { - cachedResponse: { - value: JSON.stringify(cachedResponse), - type: "object", - }, - requestId: { - value: options.requestId, - type: "string", - }, - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - }, - }); - return cachedResponse as T; - } else { - logger({ - category: "llm_cache", - message: "LLM cache miss - no cached response found", - level: 1, - auxiliary: { - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - requestId: { - value: options.requestId, - type: "string", - }, - }, - }); - } - } - const systemMessage = options.messages.find((msg) => { if (msg.role === "system") { if (typeof msg.content === "string") { @@ -309,17 +244,7 @@ export class AnthropicClient extends LLMClient { const toolUse = response.content.find((c) => c.type === "tool_use"); if (toolUse && "input" in toolUse) { const result = toolUse.input; - - const finalParsedResponse = { - data: result, - usage: usageData, - } as unknown as T; - - if (this.enableCaching) { - this.cache.set(cacheOptions, finalParsedResponse, options.requestId); - } - - return finalParsedResponse; + return result as T; } else { if (!retries || retries < 5) { return this.createChatCompletion({ @@ -345,29 +270,6 @@ export class AnthropicClient extends LLMClient { } } - if (this.enableCaching) { - this.cache.set(cacheOptions, transformedResponse, options.requestId); - logger({ - category: "anthropic", - message: "cached response", - level: 1, - auxiliary: { - requestId: { - value: options.requestId, - type: "string", - }, - transformedResponse: { - value: JSON.stringify(transformedResponse), - type: "object", - }, - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - }, - }); - } - // if the function was called with a response model, it would have returned earlier // so we can safely cast here to T, which defaults to AnthropicTransformedResponse return transformedResponse as T; diff --git a/lib/llm/CerebrasClient.ts b/lib/llm/CerebrasClient.ts index 22c4632aa..140632960 100644 --- a/lib/llm/CerebrasClient.ts +++ b/lib/llm/CerebrasClient.ts @@ -3,7 +3,6 @@ import type { ClientOptions } from "openai"; import { zodToJsonSchema } from "zod-to-json-schema"; import { LogLine } from "../../types/log"; import { AvailableModel } from "../../types/model"; -import { LLMCache } from "../cache/LLMCache"; import { ChatMessage, CreateChatCompletionOptions, @@ -14,21 +13,15 @@ import { export class CerebrasClient extends LLMClient { public type = "cerebras" as const; private client: OpenAI; - private cache: LLMCache | undefined; - private enableCaching: boolean; public clientOptions: ClientOptions; public hasVision = false; constructor({ - enableCaching = false, - cache, modelName, clientOptions, userProvidedInstructions, }: { logger: (message: LogLine) => void; - enableCaching?: boolean; - cache?: LLMCache; modelName: AvailableModel; clientOptions?: ClientOptions; userProvidedInstructions?: string; @@ -42,8 +35,6 @@ export class CerebrasClient extends LLMClient { ...clientOptions, }); - this.cache = cache; - this.enableCaching = enableCaching; this.modelName = modelName; this.clientOptions = clientOptions; } @@ -68,45 +59,6 @@ export class CerebrasClient extends LLMClient { }, }); - // Try to get cached response - const cacheOptions = { - model: this.modelName.split("cerebras-")[1], - messages: options.messages, - temperature: options.temperature, - response_model: options.response_model, - tools: options.tools, - retries: retries, - }; - - if (this.enableCaching) { - const cachedResponse = await this.cache.get( - cacheOptions, - options.requestId, - ); - if (cachedResponse) { - logger({ - category: "llm_cache", - message: "LLM cache hit - returning cached response", - level: 1, - auxiliary: { - cachedResponse: { - value: JSON.stringify(cachedResponse), - type: "object", - }, - requestId: { - value: options.requestId, - type: "string", - }, - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - }, - }); - return cachedResponse as T; - } - } - // Format messages for Cerebras API (using OpenAI format) const formattedMessages = options.messages.map((msg: ChatMessage) => { const baseMessage = { @@ -239,9 +191,6 @@ export class CerebrasClient extends LLMClient { if (toolCall?.function?.arguments) { try { const result = JSON.parse(toolCall.function.arguments); - if (this.enableCaching) { - this.cache.set(cacheOptions, result, options.requestId); - } return result as T; } catch (e) { // If JSON parse fails, the model might be returning a different format @@ -267,9 +216,6 @@ export class CerebrasClient extends LLMClient { const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { const result = JSON.parse(jsonMatch[0]); - if (this.enableCaching) { - this.cache.set(cacheOptions, result, options.requestId); - } return result as T; } } catch (e) { @@ -301,10 +247,6 @@ export class CerebrasClient extends LLMClient { ); } - if (this.enableCaching) { - this.cache.set(cacheOptions, response, options.requestId); - } - return response as T; } catch (error) { logger({ diff --git a/lib/llm/GroqClient.ts b/lib/llm/GroqClient.ts index 206dbaa07..30b8e3c9a 100644 --- a/lib/llm/GroqClient.ts +++ b/lib/llm/GroqClient.ts @@ -3,7 +3,6 @@ import OpenAI from "openai"; import { zodToJsonSchema } from "zod-to-json-schema"; import { LogLine } from "../../types/log"; import { AvailableModel } from "../../types/model"; -import { LLMCache } from "../cache/LLMCache"; import { ChatMessage, CreateChatCompletionOptions, @@ -14,21 +13,15 @@ import { export class GroqClient extends LLMClient { public type = "groq" as const; private client: OpenAI; - private cache: LLMCache | undefined; - private enableCaching: boolean; public clientOptions: ClientOptions; public hasVision = false; constructor({ - enableCaching = false, - cache, modelName, clientOptions, userProvidedInstructions, }: { logger: (message: LogLine) => void; - enableCaching?: boolean; - cache?: LLMCache; modelName: AvailableModel; clientOptions?: ClientOptions; userProvidedInstructions?: string; @@ -42,8 +35,6 @@ export class GroqClient extends LLMClient { ...clientOptions, }); - this.cache = cache; - this.enableCaching = enableCaching; this.modelName = modelName; this.clientOptions = clientOptions; } @@ -68,45 +59,6 @@ export class GroqClient extends LLMClient { }, }); - // Try to get cached response - const cacheOptions = { - model: this.modelName.split("groq-")[1], - messages: options.messages, - temperature: options.temperature, - response_model: options.response_model, - tools: options.tools, - retries: retries, - }; - - if (this.enableCaching) { - const cachedResponse = await this.cache.get( - cacheOptions, - options.requestId, - ); - if (cachedResponse) { - logger({ - category: "llm_cache", - message: "LLM cache hit - returning cached response", - level: 1, - auxiliary: { - cachedResponse: { - value: JSON.stringify(cachedResponse), - type: "object", - }, - requestId: { - value: options.requestId, - type: "string", - }, - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - }, - }); - return cachedResponse as T; - } - } - // Format messages for Groq API (using OpenAI format) const formattedMessages = options.messages.map((msg: ChatMessage) => { const baseMessage = { @@ -239,9 +191,6 @@ export class GroqClient extends LLMClient { if (toolCall?.function?.arguments) { try { const result = JSON.parse(toolCall.function.arguments); - if (this.enableCaching) { - this.cache.set(cacheOptions, result, options.requestId); - } return result as T; } catch (e) { // If JSON parse fails, the model might be returning a different format @@ -267,9 +216,6 @@ export class GroqClient extends LLMClient { const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { const result = JSON.parse(jsonMatch[0]); - if (this.enableCaching) { - this.cache.set(cacheOptions, result, options.requestId); - } return result as T; } } catch (e) { @@ -301,10 +247,6 @@ export class GroqClient extends LLMClient { ); } - if (this.enableCaching) { - this.cache.set(cacheOptions, response, options.requestId); - } - return response as T; } catch (error) { logger({ diff --git a/lib/llm/LLMProvider.ts b/lib/llm/LLMProvider.ts index 3ee3f29d8..8c7977216 100644 --- a/lib/llm/LLMProvider.ts +++ b/lib/llm/LLMProvider.ts @@ -4,7 +4,6 @@ import { ClientOptions, ModelProvider, } from "../../types/model"; -import { LLMCache } from "../cache/LLMCache"; import { AnthropicClient } from "./AnthropicClient"; import { CerebrasClient } from "./CerebrasClient"; import { GroqClient } from "./GroqClient"; @@ -32,32 +31,9 @@ const modelToProviderMap: { [key in AvailableModel]: ModelProvider } = { export class LLMProvider { private logger: (message: LogLine) => void; - private enableCaching: boolean; - private cache: LLMCache | undefined; - constructor(logger: (message: LogLine) => void, enableCaching: boolean) { + constructor(logger: (message: LogLine) => void) { this.logger = logger; - this.enableCaching = enableCaching; - this.cache = enableCaching ? new LLMCache(logger) : undefined; - } - - cleanRequestCache(requestId: string): void { - if (!this.enableCaching) { - return; - } - - this.logger({ - category: "llm_cache", - message: "cleaning up cache", - level: 1, - auxiliary: { - requestId: { - value: requestId, - type: "string", - }, - }, - }); - this.cache.deleteCacheForRequestId(requestId); } getClient( @@ -73,32 +49,24 @@ export class LLMProvider { case "openai": return new OpenAIClient({ logger: this.logger, - enableCaching: this.enableCaching, - cache: this.cache, modelName, clientOptions, }); case "anthropic": return new AnthropicClient({ logger: this.logger, - enableCaching: this.enableCaching, - cache: this.cache, modelName, clientOptions, }); case "cerebras": return new CerebrasClient({ logger: this.logger, - enableCaching: this.enableCaching, - cache: this.cache, modelName, clientOptions, }); case "groq": return new GroqClient({ logger: this.logger, - enableCaching: this.enableCaching, - cache: this.cache, modelName, clientOptions, }); diff --git a/lib/llm/OpenAIClient.ts b/lib/llm/OpenAIClient.ts index ff8e8eff8..980941a03 100644 --- a/lib/llm/OpenAIClient.ts +++ b/lib/llm/OpenAIClient.ts @@ -12,7 +12,6 @@ import { import zodToJsonSchema from "zod-to-json-schema"; import { LogLine } from "../../types/log"; import { AvailableModel } from "../../types/model"; -import { LLMCache } from "../cache/LLMCache"; import { validateZodSchema } from "../utils"; import { ChatCompletionOptions, @@ -25,27 +24,19 @@ import { export class OpenAIClient extends LLMClient { public type = "openai" as const; private client: OpenAI; - private cache: LLMCache | undefined; - private enableCaching: boolean; public clientOptions: ClientOptions; constructor({ - enableCaching = false, - cache, modelName, clientOptions, }: { logger: (message: LogLine) => void; - enableCaching?: boolean; - cache?: LLMCache; modelName: AvailableModel; clientOptions?: ClientOptions; }) { super(modelName); this.clientOptions = clientOptions; this.client = new OpenAI(clientOptions); - this.cache = cache; - this.enableCaching = enableCaching; this.modelName = modelName; } @@ -116,7 +107,7 @@ export class OpenAIClient extends LLMClient { throw new Error("Temperature is not supported for o1 models"); } - const { image, requestId, ...optionsWithoutImageAndRequestId } = options; + const { requestId, ...optionsWithoutImageAndRequestId } = options; logger({ category: "openai", @@ -137,54 +128,6 @@ export class OpenAIClient extends LLMClient { }, }); - const cacheOptions = { - model: this.modelName, - messages: options.messages, - temperature: options.temperature, - top_p: options.top_p, - frequency_penalty: options.frequency_penalty, - presence_penalty: options.presence_penalty, - image: image, - response_model: options.response_model, - }; - - if (this.enableCaching) { - const cachedResponse = await this.cache.get( - cacheOptions, - options.requestId, - ); - if (cachedResponse) { - logger({ - category: "llm_cache", - message: "LLM cache hit - returning cached response", - level: 1, - auxiliary: { - requestId: { - value: options.requestId, - type: "string", - }, - cachedResponse: { - value: JSON.stringify(cachedResponse), - type: "object", - }, - }, - }); - return cachedResponse; - } else { - logger({ - category: "llm_cache", - message: "LLM cache miss - no cached response found", - level: 1, - auxiliary: { - requestId: { - value: options.requestId, - type: "string", - }, - }, - }); - } - } - if (options.image) { const screenshotMessage: ChatMessage = { role: "user", @@ -420,43 +363,7 @@ export class OpenAIClient extends LLMClient { throw new Error("Invalid response schema"); } - if (this.enableCaching) { - this.cache.set( - cacheOptions, - { - ...parsedData, - }, - options.requestId, - ); - } - - return { - data: parsedData, - usage: response.usage, - } as T; - } - - if (this.enableCaching) { - logger({ - category: "llm_cache", - message: "caching response", - level: 1, - auxiliary: { - requestId: { - value: options.requestId, - type: "string", - }, - cacheOptions: { - value: JSON.stringify(cacheOptions), - type: "object", - }, - response: { - value: JSON.stringify(response), - type: "object", - }, - }, - }); - this.cache.set(cacheOptions, response, options.requestId); + return parsedData; } // if the function was called with a response model, it would have returned earlier diff --git a/package.json b/package.json index 61f72ce2f..75678e3e0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "prettier": "prettier --check .", "prettier:fix": "prettier --write .", "eslint": "eslint .", - "cache:clear": "rm -rf .cache", "evals": "npm run build && tsx evals/index.eval.ts", "e2e": "npm run build && cd evals/deterministic && npx playwright test --config=e2e.playwright.config.ts", "e2e:bb": "npm run build && cd evals/deterministic && npx playwright test --config=bb.playwright.config.ts",