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
5 changes: 5 additions & 0 deletions .changeset/agent-execute-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Add variables support to v3 agentExecute API schema and remove experimental requirement
28 changes: 16 additions & 12 deletions packages/core/lib/v3/agent/tools/fillFormVision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,21 @@ MANDATORY USE CASES (always use fillFormVision for these):
try {
const page = await v3.context.awaitActivePage();

// Process coordinates and substitute variables for each field
// Keep original values (with %tokens%) for logging/caching, substituted values for typing
const processedFields = fields.map((field) => {
// Process coordinates per field. Keep the original `value` (with any
// `%variableName%` tokens) so substituted secrets never leak through
// the tool result, replay cache, returned AgentResult.actions, or
// anything that logs the action. Variables are substituted only at
// typing time below.
const safeFields = fields.map((field) => {
const processed = processCoordinates(
field.coordinates.x,
field.coordinates.y,
provider,
v3,
);
return {
...field,
originalValue: field.value, // Keep original with %tokens% for cache
value: substituteVariables(field.value, variables),
action: field.action,
value: field.value,
coordinates: { x: processed.x, y: processed.y },
};
});
Expand All @@ -97,7 +99,7 @@ MANDATORY USE CASES (always use fillFormVision for these):
const shouldCollectXpath = v3.isAgentReplayActive();
const actions: Action[] = [];

for (const field of processedFields) {
for (const field of safeFields) {
// Click the field, only requesting XPath when caching is enabled
const xpath = await page.click(
field.coordinates.x,
Expand All @@ -106,18 +108,20 @@ MANDATORY USE CASES (always use fillFormVision for these):
returnXpath: shouldCollectXpath,
},
);
await page.type(field.value);
// Substitute variables only at the moment of typing so the resolved
// value never leaves this scope.
await page.type(substituteVariables(field.value, variables));

// Build Action with XPath for deterministic replay (only when caching)
// Use originalValue (with %tokens%) so cache stores references, not sensitive values
// Build Action with XPath for deterministic replay (only when caching).
// Store the placeholder value so cache entries don't contain secrets.
if (shouldCollectXpath) {
const normalizedXpath = ensureXPath(xpath);
if (normalizedXpath) {
actions.push({
selector: normalizedXpath,
description: field.action,
method: "type",
arguments: [field.originalValue],
arguments: [field.value],
});
}
}
Expand All @@ -140,7 +144,7 @@ MANDATORY USE CASES (always use fillFormVision for these):

return {
success: true,
playwrightArguments: processedFields,
playwrightArguments: safeFields,
screenshotBase64,
};
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,6 @@ export function validateExperimentalFeatures(
if (executeOptions.output) {
features.push("output schema");
}
if (
executeOptions.variables &&
Object.keys(executeOptions.variables).length > 0
) {
features.push("variables");
}
}

if (features.length > 0) {
Expand Down
3 changes: 1 addition & 2 deletions packages/core/lib/v3/types/public/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,8 @@ export interface AgentExecuteOptionsBase {
*
* Accepts both simple values and rich objects with descriptions (same type as `act`).
*
* **Note:** Not supported in CUA mode (`mode: "cua"`). Requires `experimental: true`.
* **Note:** Not supported in CUA mode (`mode: "cua"`).
*
* @experimental
* @example
* ```typescript
* // Simple values
Expand Down
4 changes: 4 additions & 0 deletions packages/core/lib/v3/types/public/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,10 @@ export const AgentExecuteOptionsSchema = z
description: "Timeout in milliseconds for each agent tool call",
example: 30000,
}),
variables: VariablesSchema.optional().meta({
description:
"Variables available to the agent via %variableName% syntax in supported tools",
}),
})
.meta({ id: "AgentExecuteOptions" });

Expand Down
49 changes: 49 additions & 0 deletions packages/core/tests/unit/agent-variables-validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { validateExperimentalFeatures } from "../../lib/v3/agent/utils/validateExperimentalFeatures.js";
import { StagehandInvalidArgumentError } from "../../lib/v3/types/public/sdkErrors.js";

describe("agent variable experimental validation", () => {
it("allows variables without experimental mode", () => {
expect(() =>
validateExperimentalFeatures({
isExperimental: false,
agentConfig: { mode: "dom" },
executeOptions: {
instruction: "fill %username%",
variables: { username: "john@example.com" },
},
}),
).not.toThrow();
});

it("allows rich variables without experimental mode", () => {
expect(() =>
validateExperimentalFeatures({
isExperimental: false,
agentConfig: { mode: "dom" },
executeOptions: {
instruction: "fill %username%",
variables: {
username: {
value: "john@example.com",
description: "The login email",
},
},
},
}),
).not.toThrow();
});

it("continues to reject variables in CUA mode", () => {
expect(() =>
validateExperimentalFeatures({
isExperimental: true,
agentConfig: { mode: "cua" },
executeOptions: {
instruction: "fill %username%",
variables: { username: "john@example.com" },
},
}),
).toThrow(StagehandInvalidArgumentError);
});
});
58 changes: 58 additions & 0 deletions packages/core/tests/unit/api-client-observe-variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,62 @@ describe("StagehandAPIClient variable serialization", () => {
serverCache: undefined,
});
});

it("preserves rich variables when sending the agentExecute request", async () => {
const client = new StagehandAPIClient({
apiKey: "bb-test",
logger: vi.fn(),
});
const executeMock = vi.fn().mockResolvedValue({
success: true,
message: "ok",
actions: [],
completed: true,
});

(
client as unknown as {
execute: typeof executeMock;
}
).execute = executeMock;

await client.agentExecute(
{ mode: "dom" },
{
instruction: "fill the form with %username% and %password%",
variables: {
username: "john@example.com",
password: {
value: "secret",
description: "The login password",
},
},
},
);

expect(executeMock).toHaveBeenCalledWith({
method: "agentExecute",
args: {
agentConfig: {
systemPrompt: undefined,
mode: "dom",
cua: undefined,
model: undefined,
executionModel: undefined,
},
executeOptions: {
instruction: "fill the form with %username% and %password%",
variables: {
username: "john@example.com",
password: {
value: "secret",
description: "The login password",
},
},
},
frameId: undefined,
shouldCache: undefined,
},
});
});
});
26 changes: 26 additions & 0 deletions packages/core/tests/unit/api-variables-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,30 @@ describe("API variable schemas", () => {

expect(result.success).toBe(true);
});

it("preserves variables for agent execute requests", () => {
const result = Api.AgentExecuteRequestSchema.safeParse({
agentConfig: { mode: "dom" },
executeOptions: {
instruction: "fill the form with %username% and %password%",
variables: {
username: "john@example.com",
password: {
value: "secret-password",
description: "The login password",
},
},
},
});

expect(result.success).toBe(true);
if (!result.success) throw result.error;
expect(result.data.executeOptions.variables).toEqual({
username: "john@example.com",
password: {
value: "secret-password",
description: "The login password",
},
});
});
});
3 changes: 1 addition & 2 deletions packages/docs/v3/basics/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -486,14 +486,13 @@ const result = await agent.execute({

Use variables to pass sensitive data (like passwords, API keys, or personal information) to the agent without exposing the actual values to the LLM. The agent sees only variable names and descriptions, while the actual values are substituted at runtime.

<Note>**Non-CUA agents only.** Variables require `experimental: true` and are not available with Computer Use Agents.</Note>
<Note>**Non-CUA agents only.** Variables are not available with Computer Use Agents.</Note>

### Basic Usage

```typescript
const stagehand = new Stagehand({
env: "LOCAL",
experimental: true, // Required for variables
});
await stagehand.init();

Expand Down
6 changes: 6 additions & 0 deletions packages/server-v3/openapi.v3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,9 @@ components:
description: Timeout in milliseconds for each agent tool call
example: 30000
type: number
variables:
description: Variables available to the agent via %variableName% syntax in supported tools
$ref: "#/components/schemas/Variables"
required:
- instruction
AgentExecuteRequest:
Expand Down Expand Up @@ -1767,6 +1770,9 @@ components:
description: Timeout in milliseconds for each agent tool call
example: 30000
type: number
variables:
description: Variables available to the agent via %variableName% syntax in supported tools
$ref: "#/components/schemas/Variables"
required:
- instruction
additionalProperties: false
Expand Down
Loading
Loading