From 8a226a12824575e7342cb92ae66333c5c1269b9f Mon Sep 17 00:00:00 2001 From: AlphaX Date: Mon, 22 Sep 2025 22:23:57 +0000 Subject: [PATCH 1/3] Allow configuring cache directory --- docs/best-practices/caching.mdx | 4 +- lib/cache/BaseCache.ts | 17 ++++++-- lib/index.ts | 11 ++++- lib/llm/LLMProvider.ts | 8 +++- tests/cacheDir.test.ts | 71 +++++++++++++++++++++++++++++++++ types/stagehand.ts | 5 +++ 6 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 tests/cacheDir.test.ts diff --git a/docs/best-practices/caching.mdx b/docs/best-practices/caching.mdx index 6835192b0..bd332b72a 100644 --- a/docs/best-practices/caching.mdx +++ b/docs/best-practices/caching.mdx @@ -5,6 +5,8 @@ description: You can cache actions in Stagehand to avoid redundant LLM calls. Caching actions in Stagehand is useful for actions that are expensive to run, or when the underlying DOM structure is not expected to change. +> Set the `cacheDir` constructor option or the `STAGEHAND_CACHE_DIR` environment variable to change where Stagehand stores cache files. This is helpful in environments where the default `/tmp/.cache` path is not writable. + ## Using `observe` to preview an action `observe` lets you preview an action before taking it. If you are satisfied with the action preview, you can run it in `page.act` with no further LLM calls. @@ -203,4 +205,4 @@ page_content = await page.content() ``` -You may also want to use the accessibility tree, the DOM, or any other information to create a more unique key. You can do this as you please, with very similar logic to the above example. \ No newline at end of file +You may also want to use the accessibility tree, the DOM, or any other information to create a more unique key. You can do this as you please, with very similar logic to the above example. diff --git a/lib/cache/BaseCache.ts b/lib/cache/BaseCache.ts index 18ef61690..0bce7ac34 100644 --- a/lib/cache/BaseCache.ts +++ b/lib/cache/BaseCache.ts @@ -31,17 +31,26 @@ export class BaseCache { constructor( logger: (message: LogLine) => void, - cacheDir: string = path.join(process.cwd(), "tmp", ".cache"), + cacheDir?: string, cacheFile: string = "cache.json", ) { this.logger = logger; - this.cacheDir = cacheDir; - this.cacheFile = path.join(cacheDir, cacheFile); - this.lockFile = path.join(cacheDir, "cache.lock"); + const resolvedCacheDir = this.resolveCacheDirectory(cacheDir); + this.cacheDir = resolvedCacheDir; + this.cacheFile = path.join(resolvedCacheDir, cacheFile); + this.lockFile = path.join(resolvedCacheDir, "cache.lock"); this.ensureCacheDirectory(); this.setupProcessHandlers(); } + private resolveCacheDirectory(override?: string): string { + const candidate = override ?? process.env.STAGEHAND_CACHE_DIR; + if (candidate && candidate.trim().length > 0) { + return path.isAbsolute(candidate) ? candidate : path.resolve(candidate); + } + return path.join(process.cwd(), "tmp", ".cache"); + } + private setupProcessHandlers(): void { const releaseLockAndExit = () => { this.releaseLock(); diff --git a/lib/index.ts b/lib/index.ts index 7a1a95470..d171973a0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -375,6 +375,7 @@ export class Stagehand { public verbose: 0 | 1 | 2; public llmProvider: LLMProvider; public enableCaching: boolean; + public readonly cacheDir?: string; protected apiKey: string | undefined; private projectId: string | undefined; private externalLogger?: (logLine: LogLine) => void; @@ -523,6 +524,7 @@ export class Stagehand { browserbaseSessionCreateParams, domSettleTimeoutMs, enableCaching, + cacheDir, browserbaseSessionID, modelName, modelClientOptions, @@ -555,8 +557,15 @@ export class Stagehand { enableCaching ?? (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true"); + const cacheDirCandidate = cacheDir ?? process.env.STAGEHAND_CACHE_DIR; + this.cacheDir = + cacheDirCandidate && cacheDirCandidate.trim().length > 0 + ? path.resolve(cacheDirCandidate) + : undefined; + this.llmProvider = - llmProvider || new LLMProvider(this.logger, this.enableCaching); + llmProvider || + new LLMProvider(this.logger, this.enableCaching, this.cacheDir); this.apiKey = apiKey ?? process.env.BROWSERBASE_API_KEY; this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID; diff --git a/lib/llm/LLMProvider.ts b/lib/llm/LLMProvider.ts index f99d6edf8..ef8296de2 100644 --- a/lib/llm/LLMProvider.ts +++ b/lib/llm/LLMProvider.ts @@ -133,10 +133,14 @@ export class LLMProvider { private enableCaching: boolean; private cache: LLMCache | undefined; - constructor(logger: (message: LogLine) => void, enableCaching: boolean) { + constructor( + logger: (message: LogLine) => void, + enableCaching: boolean, + cacheDir?: string, + ) { this.logger = logger; this.enableCaching = enableCaching; - this.cache = enableCaching ? new LLMCache(logger) : undefined; + this.cache = enableCaching ? new LLMCache(logger, cacheDir) : undefined; } cleanRequestCache(requestId: string): void { diff --git a/tests/cacheDir.test.ts b/tests/cacheDir.test.ts new file mode 100644 index 000000000..ed102ac06 --- /dev/null +++ b/tests/cacheDir.test.ts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import fs from "fs"; +import os from "os"; +import path from "path"; + +import { LLMCache } from "../lib/cache/LLMCache"; +import { LLMProvider } from "../lib/llm/LLMProvider"; +import { LogLine } from "../types/log"; + +const noopLogger: (line: LogLine) => void = () => {}; + +test("LLMCache respects explicit cache directory override", () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stagehand-cache-")); + const customDir = path.join(tmpRoot, "custom-cache"); + + try { + new LLMCache(noopLogger, customDir); + assert.ok( + fs.existsSync(customDir), + "expected custom cache directory to be created", + ); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } +}); + +test( + "LLMCache reads cache directory from STAGEHAND_CACHE_DIR env var", + () => { + const tmpRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "stagehand-env-cache-"), + ); + const envDir = path.join(tmpRoot, "env-cache"); + const previous = process.env.STAGEHAND_CACHE_DIR; + process.env.STAGEHAND_CACHE_DIR = envDir; + + try { + new LLMCache(noopLogger); + assert.ok( + fs.existsSync(envDir), + "expected env-configured cache directory to be created", + ); + } finally { + if (previous === undefined) { + delete process.env.STAGEHAND_CACHE_DIR; + } else { + process.env.STAGEHAND_CACHE_DIR = previous; + } + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }, +); + +test("LLMProvider skips creating cache directory when caching disabled", () => { + const tmpRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "stagehand-provider-cache-"), + ); + const disabledDir = path.join(tmpRoot, "disabled-cache"); + + try { + new LLMProvider(noopLogger, false, disabledDir); + assert.ok( + !fs.existsSync(disabledDir), + "expected cache directory to be absent when caching is disabled", + ); + } finally { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } +}); + diff --git a/types/stagehand.ts b/types/stagehand.ts index 50f2ac2a5..4696093d0 100644 --- a/types/stagehand.ts +++ b/types/stagehand.ts @@ -57,6 +57,11 @@ export interface ConstructorParams { * @default true */ enableCaching?: boolean; + /** + * Directory used to persist cache files when caching is enabled + * @default `/tmp/.cache` + */ + cacheDir?: string; /** * The ID of a Browserbase session to resume */ From 164e9aac1c890d84af37340a032f4f7c4137a346 Mon Sep 17 00:00:00 2001 From: TR-3B <144127816+MagellaX@users.noreply.github.com> Date: Tue, 23 Sep 2025 04:04:27 +0530 Subject: [PATCH 2/3] Update lib/cache/BaseCache.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- lib/cache/BaseCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cache/BaseCache.ts b/lib/cache/BaseCache.ts index 0bce7ac34..92df28d4f 100644 --- a/lib/cache/BaseCache.ts +++ b/lib/cache/BaseCache.ts @@ -45,7 +45,7 @@ export class BaseCache { private resolveCacheDirectory(override?: string): string { const candidate = override ?? process.env.STAGEHAND_CACHE_DIR; - if (candidate && candidate.trim().length > 0) { + if (candidate?.trim()) { return path.isAbsolute(candidate) ? candidate : path.resolve(candidate); } return path.join(process.cwd(), "tmp", ".cache"); From 4aedc7079968705541c3a1b6a1a6e1c2ec4d8209 Mon Sep 17 00:00:00 2001 From: TR-3B <144127816+MagellaX@users.noreply.github.com> Date: Tue, 23 Sep 2025 04:04:35 +0530 Subject: [PATCH 3/3] Update lib/index.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- lib/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index d171973a0..e8feedfaa 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -558,10 +558,7 @@ export class Stagehand { (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true"); const cacheDirCandidate = cacheDir ?? process.env.STAGEHAND_CACHE_DIR; - this.cacheDir = - cacheDirCandidate && cacheDirCandidate.trim().length > 0 - ? path.resolve(cacheDirCandidate) - : undefined; + this.cacheDir = cacheDirCandidate ? path.resolve(cacheDirCandidate) : undefined; this.llmProvider = llmProvider ||