Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/best-practices/caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cwd>/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.

Expand Down Expand Up @@ -203,4 +205,4 @@ page_content = await page.content()
```
</CodeGroup>

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.
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.
17 changes: 13 additions & 4 deletions lib/cache/BaseCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,26 @@ export class BaseCache<T extends CacheEntry> {

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?.trim()) {
return path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
}
return path.join(process.cwd(), "tmp", ".cache");
}

private setupProcessHandlers(): void {
const releaseLockAndExit = () => {
this.releaseLock();
Expand Down
8 changes: 7 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -523,6 +524,7 @@ export class Stagehand {
browserbaseSessionCreateParams,
domSettleTimeoutMs,
enableCaching,
cacheDir,
browserbaseSessionID,
modelName,
modelClientOptions,
Expand Down Expand Up @@ -555,8 +557,12 @@ export class Stagehand {
enableCaching ??
(process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true");

const cacheDirCandidate = cacheDir ?? process.env.STAGEHAND_CACHE_DIR;
this.cacheDir = cacheDirCandidate ? 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;

Expand Down
8 changes: 6 additions & 2 deletions lib/llm/LLMProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
71 changes: 71 additions & 0 deletions tests/cacheDir.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});

5 changes: 5 additions & 0 deletions types/stagehand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export interface ConstructorParams {
* @default true
*/
enableCaching?: boolean;
/**
* Directory used to persist cache files when caching is enabled
* @default `<cwd>/tmp/.cache`
*/
cacheDir?: string;
/**
* The ID of a Browserbase session to resume
*/
Expand Down