Skip to content

Commit 8543c11

Browse files
github-actions[bot]a7med3liamintrillvilleclaudepirate
authored
[Claimed browserbase#1978] fix: add variable substitution to keys step during cache replay (browserbase#1983)
Mirrored from external contributor PR browserbase#1978 after approval by @pirate. Original author: @a7med3liamin Original PR: browserbase#1978 Approved source head SHA: `2149aa265a04dc37154d5a84411f3ab4d1045897` @a7med3liamin, please continue any follow-up discussion on this mirrored PR. When the external PR gets new commits, this same internal PR will be marked stale until the latest external commit is approved and refreshed here. ## Original description - [x] Check the [documentation](https://docs.stagehand.dev/) for relevant information - [x] Search existing [issues](https://github.com/browserbase/stagehand/issues) to avoid duplicates Fixes browserbase#1776 ## Problem The `keys` tool has no variable substitution in either the live execution or cache replay paths. When the agent uses `%variableName%` tokens with the keys tool, the literal token string gets typed instead of the resolved value. ## Fix This PR combines two fixes into one: ### 1. Live execution (original fix by @trillville from browserbase#1777) - Accept `variables` parameter in `keysTool` (matching `typeTool`) - Call `substituteVariables()` before `page.type()` in the `method === "type"` branch - Pass `variables` to `keysTool` in `createAgentTools` - Update schema description to advertise available variables to the LLM - Return original token in result to avoid exposing sensitive values to LLM ### 2. Cache replay (new fix) - Import `substituteVariables` in `AgentCache.ts` - Pass `variables` through to `replayAgentKeysStep` - Call `substituteVariables(text, variables)` before `page.type()` in the replay path Without fix #2, cached `keys` steps with `method="type"` replay by typing literal `%variableName%` tokens even when variables are provided, since `replayAgentKeysStep` had no access to the variables map. ## Credit The live execution fix (part 1) is from @trillville's work in browserbase#1777/browserbase#1813. We merged it here with the cache replay fix per @pirate's request to consolidate into a single PR. <!-- external-contributor-pr:owned source-pr=1978 source-sha=2149aa265a04dc37154d5a84411f3ab4d1045897 claimer=pirate --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add variable substitution to the `keys` tool for both live execution and cache replay so `%variableName%` tokens are resolved before typing. This fixes cases where literal tokens were typed and brings parity with the `type` tool. - **Bug Fixes** - Pass `variables` into `keys` and call `substituteVariables()` before `page.type()`; update the input schema to list available variables. - In cache replay, forward `variables` to `replayAgentKeysStep` and substitute before typing to avoid replaying literal tokens. - Record and return the original tokenized value (not the resolved value) to avoid leaking sensitive data. <sup>Written for commit abb3905. Summary will update on new commits. <a href="https://cubic.dev/pr/browserbase/stagehand/pull/1983">Review in cubic</a></sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Ahmed Ali <a7med3liamin@gmail.com> Co-authored-by: trillville <trillville@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nick Sweeting <git@sweeting.me>
1 parent c76beeb commit 8543c11

4 files changed

Lines changed: 30 additions & 11 deletions

File tree

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+
Add variable substitution to the keys tool in both live execution and cache replay paths. When keys steps with `method="type"` contain `%variableName%` tokens, they are now resolved against the provided variables. This brings the keys tool to parity with the type tool's variable handling.

packages/core/lib/v3/agent/tools/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export function createAgentTools(v3: V3, options?: V3AgentToolOptions) {
168168
fillForm: fillFormTool(v3, executionModel, variables, toolTimeout),
169169
fillFormVision: fillFormVisionTool(v3, provider, variables),
170170
goto: gotoTool(v3),
171-
keys: keysTool(v3),
171+
keys: keysTool(v3, variables),
172172
navback: navBackTool(v3),
173173
screenshot: screenshotTool(v3),
174174
scroll: mode === "hybrid" ? scrollVisionTool(v3, provider) : scrollTool(v3),

packages/core/lib/v3/agent/tools/keys.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import { tool } from "ai";
22
import { z } from "zod";
33
import type { V3 } from "../../v3.js";
4+
import type { Variables } from "../../types/public/agent.js";
5+
import { substituteVariables } from "../utils/variables.js";
46

5-
export const keysTool = (v3: V3) =>
6-
tool({
7+
export const keysTool = (v3: V3, variables?: Variables) => {
8+
const hasVariables = variables && Object.keys(variables).length > 0;
9+
const valueDescription = hasVariables
10+
? `The text to type, or the key/combo to press (Enter, Tab, Cmd+A). Use %variableName% to substitute a variable value. Available: ${Object.keys(variables).join(", ")}`
11+
: "The text to type, or the key/combo to press (Enter, Tab, Cmd+A)";
12+
13+
return tool({
714
description: `Send keyboard input to the page without targeting a specific element. Unlike the type tool which clicks then types into coordinates, this sends keystrokes directly to wherever focus currently is.
815
916
Use method="type" to enter text into the currently focused element. Preferred when: input is already focused, text needs to flow across multiple fields (e.g., verification codes)
1017
1118
Use method="press" for navigation keys (Enter, Tab, Escape, Backspace, arrows) and keyboard shortcuts (Cmd+A, Ctrl+C, Shift+Tab).`,
1219
inputSchema: z.object({
1320
method: z.enum(["press", "type"]),
14-
value: z
15-
.string()
16-
.describe(
17-
"The text to type, or the key/combo to press (Enter, Tab, Cmd+A)",
18-
),
21+
value: z.string().describe(valueDescription),
1922
repeat: z.number().optional(),
2023
}),
2124
execute: async ({ method, value, repeat }) => {
@@ -36,14 +39,17 @@ Use method="press" for navigation keys (Enter, Tab, Escape, Backspace, arrows) a
3639
const times = Math.max(1, repeat ?? 1);
3740

3841
if (method === "type") {
42+
// Substitute any %variableName% tokens in the value
43+
const actualValue = substituteVariables(value, variables);
3944
for (let i = 0; i < times; i++) {
40-
await page.type(value, { delay: 100 });
45+
await page.type(actualValue, { delay: 100 });
4146
}
4247
v3.recordAgentReplayStep({
4348
type: "keys",
4449
instruction: `type "${value}"`,
4550
playwrightArguments: { method, text: value, times },
4651
});
52+
// Return original value (with %variableName% tokens) to avoid exposing sensitive values to LLM
4753
return { success: true, method, value, times };
4854
}
4955

@@ -65,3 +71,4 @@ Use method="press" for navigation keys (Enter, Tab, Escape, Backspace, arrows) a
6571
}
6672
},
6773
});
74+
};

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
safeGetPageUrl,
3535
waitForCachedSelector,
3636
} from "./utils.js";
37+
import { substituteVariables } from "../agent/utils/variables.js";
3738

3839
const SENSITIVE_CONFIG_KEYS = new Set(["apikey", "api_key", "api-key"]);
3940

@@ -660,7 +661,11 @@ export class AgentCache {
660661
await this.replayAgentNavBackStep(step as AgentReplayNavBackStep, ctx);
661662
return step;
662663
case "keys":
663-
await this.replayAgentKeysStep(step as AgentReplayKeysStep, ctx);
664+
await this.replayAgentKeysStep(
665+
step as AgentReplayKeysStep,
666+
ctx,
667+
variables,
668+
);
664669
return step;
665670
case "done":
666671
case "extract":
@@ -811,14 +816,16 @@ export class AgentCache {
811816
private async replayAgentKeysStep(
812817
step: AgentReplayKeysStep,
813818
ctx: V3Context,
819+
variables?: Record<string, string>,
814820
): Promise<void> {
815821
const page = await ctx.awaitActivePage();
816822
const { method, text, keys, times } = step.playwrightArguments;
817823
const repeatCount = Math.max(1, times ?? 1);
818824

819825
if (method === "type" && text) {
826+
const resolvedText = substituteVariables(text, variables);
820827
for (let i = 0; i < repeatCount; i++) {
821-
await page.type(text, { delay: 100 });
828+
await page.type(resolvedText, { delay: 100 });
822829
}
823830
} else if (method === "press" && keys) {
824831
for (let i = 0; i < repeatCount; i++) {

0 commit comments

Comments
 (0)