Skip to content

Commit e822f5a

Browse files
allow act cache hit when variable values change (#1408)
# why # what changed # test plan <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Allow act() cache hits even when variable values change by caching on variable keys and resolving values at execution time. Addresses STG-1065. - **Bug Fixes** - Cache key now uses sorted variableKeys, not variable values; entries store variableKeys. - Cache hit requires matching variable keys and provided values; logs a miss when values are missing. - Variables are passed into action execution and resolved at runtime; cached actions keep placeholder arguments. - Updated types to make variables optional and include variableKeys; write cache entries with variableKeys. <sup>Written for commit 590fc8b. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent 0d2b398 commit e822f5a

File tree

5 files changed

+91
-26
lines changed

5 files changed

+91
-26
lines changed

.changeset/fruity-badgers-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
allow for act() cache hit when variable values change

packages/core/lib/v3/cache/ActCache.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,21 @@ export class ActCache {
4444
): Promise<ActCacheContext | null> {
4545
if (!this.enabled) return null;
4646
const sanitizedInstruction = instruction.trim();
47-
const sanitizedVariables = variables ? { ...variables } : {};
47+
const sanitizedVariables = variables ? { ...variables } : undefined;
48+
const variableKeys = sanitizedVariables
49+
? Object.keys(sanitizedVariables).sort()
50+
: [];
4851
const pageUrl = await safeGetPageUrl(page);
4952
const cacheKey = this.buildActCacheKey(
5053
sanitizedInstruction,
5154
pageUrl,
52-
sanitizedVariables,
55+
variableKeys,
5356
);
5457
return {
5558
instruction: sanitizedInstruction,
5659
cacheKey,
5760
pageUrl,
61+
variableKeys,
5862
variables: sanitizedVariables,
5963
};
6064
}
@@ -88,6 +92,31 @@ export class ActCache {
8892
return null;
8993
}
9094

95+
const entryVariableKeys = Array.isArray(entry.variableKeys)
96+
? [...entry.variableKeys].sort()
97+
: [];
98+
const contextVariableKeys = [...context.variableKeys];
99+
100+
if (!this.doVariableKeysMatch(entryVariableKeys, contextVariableKeys)) {
101+
return null;
102+
}
103+
104+
if (
105+
contextVariableKeys.length > 0 &&
106+
(!context.variables ||
107+
!this.hasAllVariableValues(contextVariableKeys, context.variables))
108+
) {
109+
this.logger({
110+
category: "cache",
111+
message: "act cache miss: missing variables for replay",
112+
level: 2,
113+
auxiliary: {
114+
instruction: { value: context.instruction, type: "string" },
115+
},
116+
});
117+
return null;
118+
}
119+
91120
this.logger({
92121
category: "cache",
93122
message: "act cache hit",
@@ -111,7 +140,7 @@ export class ActCache {
111140
version: 1,
112141
instruction: context.instruction,
113142
url: context.pageUrl,
114-
variables: context.variables,
143+
variableKeys: context.variableKeys,
115144
actions: result.actions ?? [],
116145
actionDescription: result.actionDescription,
117146
message: result.message,
@@ -147,12 +176,12 @@ export class ActCache {
147176
private buildActCacheKey(
148177
instruction: string,
149178
url: string,
150-
variables: Record<string, string>,
179+
variableKeys: string[],
151180
): string {
152181
const payload = JSON.stringify({
153182
instruction,
154183
url,
155-
variables,
184+
variableKeys,
156185
});
157186
return createHash("sha256").update(payload).digest("hex");
158187
}
@@ -176,6 +205,8 @@ export class ActCache {
176205
page,
177206
this.domSettleTimeoutMs,
178207
this.getDefaultLlmClient(),
208+
undefined,
209+
context.variables,
179210
);
180211
actionResults.push(result);
181212
if (!result.success) {
@@ -278,7 +309,10 @@ export class ActCache {
278309
): Promise<void> {
279310
const { error, path } = await this.storage.writeJson(
280311
`${context.cacheKey}.json`,
281-
entry,
312+
{
313+
...entry,
314+
variableKeys: context.variableKeys,
315+
},
282316
);
283317

284318
if (error && path) {
@@ -304,6 +338,35 @@ export class ActCache {
304338
});
305339
}
306340

341+
private doVariableKeysMatch(
342+
entryKeys: string[],
343+
contextKeys: string[],
344+
): boolean {
345+
if (entryKeys.length !== contextKeys.length) {
346+
return false;
347+
}
348+
349+
for (let i = 0; i < entryKeys.length; i += 1) {
350+
if (entryKeys[i] !== contextKeys[i]) {
351+
return false;
352+
}
353+
}
354+
355+
return true;
356+
}
357+
358+
private hasAllVariableValues(
359+
variableKeys: string[],
360+
variables: Record<string, string>,
361+
): boolean {
362+
for (const key of variableKeys) {
363+
if (!(key in variables)) {
364+
return false;
365+
}
366+
}
367+
return true;
368+
}
369+
307370
private async runWithTimeout<T>(
308371
run: () => Promise<T>,
309372
timeout?: number,

packages/core/lib/v3/handlers/actHandler.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,12 @@ export class ActHandler {
9797
domElements,
9898
xpathMap,
9999
llmClient,
100-
variables,
101100
requireMethodAndArguments = true,
102101
}: {
103102
instruction: string;
104103
domElements: string;
105104
xpathMap: Record<string, string>;
106105
llmClient: LLMClient;
107-
variables?: Record<string, string>;
108106
requireMethodAndArguments?: boolean;
109107
}): Promise<{ action?: Action; response: ActInferenceResponse }> {
110108
const response = await actInference({
@@ -128,16 +126,8 @@ export class ActHandler {
128126
return { response };
129127
}
130128

131-
const action: Action = {
132-
...normalized,
133-
arguments: substituteVariablesInArguments(
134-
normalized.arguments,
135-
variables,
136-
),
137-
} as Action;
138-
139129
return {
140-
action,
130+
action: { ...normalized } as Action,
141131
response,
142132
};
143133
}
@@ -178,7 +168,6 @@ export class ActHandler {
178168
domElements: combinedTree,
179169
xpathMap: combinedXpathMap,
180170
llmClient,
181-
variables,
182171
});
183172

184173
if (!firstAction) {
@@ -203,6 +192,7 @@ export class ActHandler {
203192
this.defaultDomSettleTimeoutMs,
204193
llmClient,
205194
ensureTimeRemaining,
195+
variables,
206196
);
207197

208198
// If not two-step, return the first action result
@@ -245,7 +235,6 @@ export class ActHandler {
245235
domElements: diffedTree,
246236
xpathMap: combinedXpathMap2,
247237
llmClient,
248-
variables,
249238
});
250239

251240
if (!secondAction) {
@@ -260,6 +249,7 @@ export class ActHandler {
260249
this.defaultDomSettleTimeoutMs,
261250
llmClient,
262251
ensureTimeRemaining,
252+
variables,
263253
);
264254

265255
// Combine results
@@ -282,6 +272,7 @@ export class ActHandler {
282272
domSettleTimeoutMs?: number,
283273
llmClientOverride?: LLMClient,
284274
ensureTimeRemaining?: () => void,
275+
variables?: Record<string, string>,
285276
): Promise<ActResult> {
286277
ensureTimeRemaining?.();
287278
const settleTimeout = domSettleTimeoutMs ?? this.defaultDomSettleTimeoutMs;
@@ -305,7 +296,11 @@ export class ActHandler {
305296
};
306297
}
307298

308-
const args = Array.isArray(action.arguments) ? action.arguments : [];
299+
const placeholderArgs = Array.isArray(action.arguments)
300+
? [...action.arguments]
301+
: [];
302+
const resolvedArgs =
303+
substituteVariablesInArguments(action.arguments, variables) ?? [];
309304

310305
try {
311306
ensureTimeRemaining?.();
@@ -314,7 +309,7 @@ export class ActHandler {
314309
page.mainFrame(),
315310
method,
316311
action.selector,
317-
args,
312+
resolvedArgs,
318313
settleTimeout,
319314
);
320315
return {
@@ -326,7 +321,7 @@ export class ActHandler {
326321
selector: action.selector,
327322
description: action.description || `action (${method})`,
328323
method,
329-
arguments: args,
324+
arguments: placeholderArgs,
330325
},
331326
],
332327
};
@@ -406,7 +401,7 @@ export class ActHandler {
406401
page.mainFrame(),
407402
method,
408403
newSelector,
409-
args,
404+
resolvedArgs,
410405
settleTimeout,
411406
);
412407

@@ -419,7 +414,7 @@ export class ActHandler {
419414
selector: newSelector,
420415
description: action.description || `action (${method})`,
421416
method,
422-
arguments: args,
417+
arguments: placeholderArgs,
423418
},
424419
],
425420
};

packages/core/lib/v3/types/private/cache.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export type ActCacheContext = {
4141
instruction: string;
4242
cacheKey: string;
4343
pageUrl: string;
44-
variables: Record<string, string>;
44+
variableKeys: string[];
45+
variables?: Record<string, string>;
4546
};
4647

4748
export type ActCacheDeps = {
@@ -67,7 +68,7 @@ export interface CachedActEntry {
6768
version: 1;
6869
instruction: string;
6970
url: string;
70-
variables: Record<string, string>;
71+
variableKeys: string[];
7172
actions: Action[];
7273
actionDescription?: string;
7374
message?: string;

packages/core/lib/v3/v3.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ export class V3 {
10281028
this.domSettleTimeoutMs,
10291029
this.resolveLlmClient(options?.model),
10301030
ensureTimeRemaining,
1031+
options?.variables,
10311032
);
10321033
}
10331034

0 commit comments

Comments
 (0)