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
2 changes: 1 addition & 1 deletion packages/evals/ARCHITECTURE.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ flowchart TB
CoreContext["framework/context.ts<br/>buildCoreContext"]
FixtureServer["core/fixtures<br/>local deterministic pages"]
CoreTargets["core/targets<br/>local Chrome<br/>Browserbase CDP"]
CoreTools["core/tools registry<br/>understudy_code<br/>playwright_code<br/>cdp_code<br/>playwright_mcp<br/>chrome_devtools_mcp<br/>browse_cli"]
CoreTools["core/tools registry<br/>understudy_code<br/>playwright_code<br/>cdp_code<br/>playwright_mcp<br/>chrome_devtools_mcp<br/>stagehand_facade<br/>browse_cli"]
CoreAssertions["assertions + metrics<br/>adapter-backed results"]
CoreDeps["core/runtime/coreDeps.ts<br/>browserbase + ws<br/>lazy require"]
end
Expand Down
1 change: 1 addition & 0 deletions packages/evals/core/contracts/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type ToolSurface =
| "cdp_code"
| "playwright_mcp"
| "chrome_devtools_mcp"
| "stagehand_facade"
| "browse_cli";

export type StartupProfile =
Expand Down
4 changes: 4 additions & 0 deletions packages/evals/core/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChromeDevtoolsMcpTool } from "./chrome_devtools_mcp.js";
import { PlaywrightCodeTool } from "./playwright_code.js";
import { PlaywrightMcpTool } from "./playwright_mcp.js";
import { StagehandCodeTool } from "./stagehand_code.js";
import { StagehandFacadeTool } from "./stagehand_facade.js";
import { UnderstudyCodeTool } from "./understudy_code.js";

export function listCoreTools(): ToolSurface[] {
Expand All @@ -15,6 +16,7 @@ export function listCoreTools(): ToolSurface[] {
"cdp_code",
"playwright_mcp",
"chrome_devtools_mcp",
"stagehand_facade",
"browse_cli",
];
}
Expand All @@ -33,6 +35,8 @@ export function getCoreTool(toolSurface: ToolSurface): CoreTool {
return new PlaywrightMcpTool();
case "chrome_devtools_mcp":
return new ChromeDevtoolsMcpTool();
case "stagehand_facade":
return new StagehandFacadeTool();
case "browse_cli":
return new BrowseCliTool();
default:
Expand Down
135 changes: 135 additions & 0 deletions packages/evals/core/tools/stagehand_facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { FACADE_AGENT_INSTRUCTIONS } from "@browserbasehq/stagehand-integrations/facade";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { fileURLToPath } from "node:url";
import type { Artifact, ConnectionMode } from "../contracts/results.js";
import type {
CoreCapability,
CorePageHandle,
CoreSession,
CoreTool,
StartupProfile,
ToolStartInput,
ToolStartResult,
} from "../contracts/tool.js";
import type { TargetKind } from "../contracts/targets.js";

const SUPPORTED_CAPABILITIES: CoreCapability[] = [
"session",
"navigation",
"evaluation",
"screenshot",
"viewport",
"wait",
"click",
"hover",
"scroll",
"type",
"press",
"tabs",
"representation",
];

const serverPath = fileURLToPath(
import.meta.resolve("@browserbasehq/stagehand-integrations/facade/stdio-server"),
);

function unsupportedSessionOperation(): never {
throw new Error("stagehand_facade is available only through its agent MCP mount");
}

/**
* The facade process and its browser are spawned by the agent harness. This
* placeholder satisfies the CoreTool lifecycle contract without launching a
* second, unobservable browser solely for the runner-side session.
*/
class StagehandFacadeMountSession implements CoreSession {
async listPages(): Promise<CorePageHandle[]> {
return unsupportedSessionOperation();
}

async activePage(): Promise<CorePageHandle> {
return unsupportedSessionOperation();
}

async newPage(): Promise<CorePageHandle> {
return unsupportedSessionOperation();
}

async selectPage(): Promise<void> {
unsupportedSessionOperation();
}

async closePage(): Promise<void> {
unsupportedSessionOperation();
}

async close(): Promise<void> {}

async getArtifacts(): Promise<Artifact[]> {
return [];
}

async getRawMetrics(): Promise<Record<string, unknown>> {
return {};
}
}

export function buildStagehandFacadeEnv(
environment: ToolStartInput["environment"],
): Record<string, string> {
return {
...buildAllowlistedEnv(),
STAGEHAND_BROWSER: environment === "BROWSERBASE" ? "browserbase" : "local",
};
}

function connectionModeFromProfile(startupProfile: StartupProfile): ConnectionMode {
return startupProfile === "tool_create_browserbase" ? "browserbase_native" : "launch";
}

export class StagehandFacadeTool implements CoreTool {
readonly id = "stagehand_facade";
readonly surface = "mcp";
readonly family = "stagehand";
readonly supportedStartupProfiles: StartupProfile[] = [
"tool_launch_local",
"tool_create_browserbase",
];
readonly supportedCapabilities: CoreCapability[] = [...SUPPORTED_CAPABILITIES];
readonly supportedTargetKinds: TargetKind[] = ["selector", "coords", "focused", "snapshot_ref"];

async start(input: ToolStartInput): Promise<ToolStartResult> {
const expectedProfile =
input.environment === "BROWSERBASE" ? "tool_create_browserbase" : "tool_launch_local";
if (input.startupProfile !== expectedProfile) {
throw new Error(
`stagehand_facade startup profile "${input.startupProfile}" is not valid for environment "${input.environment}"`,
);
}

const session = new StagehandFacadeMountSession();
return {
session,
agentMount: {
via: "mcp",
promptInstructions: FACADE_AGENT_INSTRUCTIONS,
mcpServers: {
stagehand: {
command: process.execPath,
args: [serverPath],
env: buildStagehandFacadeEnv(input.environment),
Comment thread
miguelg719 marked this conversation as resolved.
},
},
},
cleanup: async () => {
await session.close();
},
metadata: {
environment: input.environment === "BROWSERBASE" ? "browserbase" : "local",
browserOwnership: "tool",
connectionMode: connectionModeFromProfile(input.startupProfile),
startupProfile: input.startupProfile,
},
};
}
}
25 changes: 15 additions & 10 deletions packages/evals/framework/claudeCodeToolAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ export async function prepareClaudeCodeToolAdapter(
case "cdp_code":
case "stagehand_code":
case "playwright_mcp":
case "chrome_devtools_mcp": {
case "chrome_devtools_mcp":
case "stagehand_facade": {
return prepareMountedCoreToolAdapter({
...input,
toolSurface,
Expand All @@ -187,7 +188,7 @@ export async function prepareClaudeCodeToolAdapter(
}
default:
throw new EvalsError(
`Claude Code harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp for execution right now; received "${toolSurface}".`,
`Claude Code harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp, with stagehand_facade also available; received "${toolSurface}".`,
);
}
}
Expand All @@ -200,12 +201,13 @@ export function resolveClaudeCodeToolSurface(requested?: ToolSurface): ToolSurfa
requested === "cdp_code" ||
requested === "stagehand_code" ||
requested === "playwright_mcp" ||
requested === "chrome_devtools_mcp"
requested === "chrome_devtools_mcp" ||
requested === "stagehand_facade"
) {
return requested;
}
throw new EvalsError(
`Claude Code harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp for execution right now; received "${requested}".`,
`Claude Code harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp, with stagehand_facade also available; received "${requested}".`,
);
}

Expand All @@ -216,14 +218,17 @@ export function resolveClaudeCodeStartupProfile(
): StartupProfile {
if (requested) return requested;

// browse_cli and stagehand_code own their browser (the Stagehand SDK launches or
// creates it via the extension stack), so no runner-provided CDP endpoint.
if (toolSurface === "browse_cli" || toolSurface === "stagehand_code") {
// browse_cli, stagehand_code, and stagehand_facade own their browser (the
// Stagehand SDK launches or creates it), so no runner-provided CDP endpoint.
if (
toolSurface === "browse_cli" ||
toolSurface === "stagehand_code" ||
toolSurface === "stagehand_facade"
) {
return environment === "BROWSERBASE" ? "tool_create_browserbase" : "tool_launch_local";
}
// The MCP surfaces need a runner-provided endpoint so the harness-side
// session (evidence capture) and the agent's own server instance attach to
// the same browser.
// The attachable surfaces need a runner-provided endpoint so the harness-side
// session (evidence capture) and the agent's server instance share a browser.
if (
toolSurface === "playwright_code" ||
toolSurface === "cdp_code" ||
Expand Down
22 changes: 15 additions & 7 deletions packages/evals/framework/codexToolAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export interface PreparedCodexCodeAdapter {
export type PreparedCodexToolAdapter = PreparedBrowseCliHarnessAdapter | PreparedCodexCodeAdapter;

const CODE_SURFACES = new Set<ToolSurface>(["stagehand_code", "playwright_code", "cdp_code"]);
const MCP_SURFACES = new Set<ToolSurface>(["playwright_mcp", "chrome_devtools_mcp"]);
const MCP_SURFACES = new Set<ToolSurface>([
"playwright_mcp",
"chrome_devtools_mcp",
"stagehand_facade",
Comment thread
miguelg719 marked this conversation as resolved.
]);

/** Mirrors the claude adapter's bounded, best-effort terminal capture. */
function boundedCaptureEvidence(
Expand Down Expand Up @@ -281,7 +285,7 @@ export function resolveCodexToolSurface(requested?: ToolSurface): ToolSurface {
return requested;
}
throw new EvalsError(
`Codex harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp for execution right now; received "${requested}".`,
`Codex harness supports --tool browse_cli, playwright_code, cdp_code, stagehand_code, playwright_mcp, or chrome_devtools_mcp, with stagehand_facade also available; received "${requested}".`,
);
}

Expand All @@ -292,13 +296,17 @@ export function resolveCodexStartupProfile(
): StartupProfile {
if (requested) return requested;

// browse_cli and stagehand_code own their browser; playwright/cdp attach to a
// runner-provided CDP endpoint (same defaults as the claude_code harness).
if (toolSurface === "browse_cli" || toolSurface === "stagehand_code") {
// browse_cli, stagehand_code, and stagehand_facade own their browser;
// playwright/cdp attach to a runner-provided CDP endpoint.
if (
toolSurface === "browse_cli" ||
toolSurface === "stagehand_code" ||
toolSurface === "stagehand_facade"
) {
return environment === "BROWSERBASE" ? "tool_create_browserbase" : "tool_launch_local";
}
// MCP surfaces need a runner-provided endpoint so the harness-side session
// (evidence capture) and the agent's own server instance share one browser.
// Attachable surfaces need a runner-provided endpoint so the harness-side
// session (evidence capture) and the agent's server instance share a browser.
if (
toolSurface === "playwright_code" ||
toolSurface === "cdp_code" ||
Expand Down
1 change: 1 addition & 0 deletions packages/evals/framework/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function resolveDefaultCoreStartupProfile(
): StartupProfile {
switch (toolSurface) {
case "browse_cli":
case "stagehand_facade":
Comment thread
miguelg719 marked this conversation as resolved.
return environment === "BROWSERBASE" ? "tool_create_browserbase" : "tool_launch_local";
case "understudy_code":
case "playwright_code":
Expand Down
97 changes: 97 additions & 0 deletions packages/evals/tests/core/stagehand-facade.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { FACADE_AGENT_INSTRUCTIONS } from "@browserbasehq/stagehand-integrations/facade";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getCoreTool, listCoreTools } from "../../core/tools/registry.js";
import { buildStagehandFacadeEnv, StagehandFacadeTool } from "../../core/tools/stagehand_facade.js";
import {
resolveClaudeCodeStartupProfile,
resolveClaudeCodeToolSurface,
} from "../../framework/claudeCodeToolAdapter.js";
import {
resolveCodexStartupProfile,
resolveCodexToolSurface,
} from "../../framework/codexToolAdapter.js";
import type { EvalLogger } from "../../logger.js";

const ORIGINAL_ENV = { ...process.env };

beforeEach(() => {
for (const key of Object.keys(process.env)) {
if (/^(STAGEHAND_|BROWSERBASE_)/u.test(key)) delete process.env[key];
}
});

afterEach(() => {
for (const key of Object.keys(process.env)) {
if (!(key in ORIGINAL_ENV)) delete process.env[key];
}
Object.assign(process.env, ORIGINAL_ENV);
});

describe("stagehand facade tool surface", () => {
it("is registered", () => {
expect(listCoreTools()).toContain("stagehand_facade");
expect(getCoreTool("stagehand_facade")).toBeInstanceOf(StagehandFacadeTool);
});

it("builds the shipped facade MCP mount", async () => {
process.env.STAGEHAND_MODEL_NAME = "openai/gpt-5-mini";
process.env.BROWSERBASE_API_KEY = "browserbase-secret";
process.env.OPENAI_API_KEY = "must-not-cross-the-mount";

const running = await new StagehandFacadeTool().start({
logger: {} as EvalLogger,
environment: "LOCAL",
startupProfile: "tool_launch_local",
});

expect(running.agentMount?.via).toBe("mcp");
if (running.agentMount?.via !== "mcp") throw new Error("expected MCP mount");
expect(running.agentMount.promptInstructions).toBe(FACADE_AGENT_INSTRUCTIONS);
expect(Object.keys(running.agentMount.mcpServers)).toEqual(["stagehand"]);
expect(running.agentMount.mcpServers.stagehand).toMatchObject({
command: process.execPath,
args: [expect.stringMatching(/facade[/\\]stdio-server\.mjs$/u)],
env: {
STAGEHAND_BROWSER: "local",
STAGEHAND_MODEL_NAME: "openai/gpt-5-mini",
BROWSERBASE_API_KEY: "browserbase-secret",
},
});
expect(
(running.agentMount.mcpServers.stagehand as { env: Record<string, string> }).env,
).not.toHaveProperty("OPENAI_API_KEY");
expect(running.captureEvidence).toBeUndefined();
await running.cleanup();
});

it("filters host env and overrides browser selection for each eval environment", () => {
process.env.STAGEHAND_BROWSER = "browserbase";
process.env.STAGEHAND_MODEL_API_KEY = "model-secret";
process.env.BROWSERBASE_PROJECT_ID = "project-id";
process.env.ANTHROPIC_API_KEY = "must-not-cross-the-mount";

expect(buildStagehandFacadeEnv("LOCAL")).toEqual({
STAGEHAND_BROWSER: "local",
STAGEHAND_MODEL_API_KEY: "model-secret",
BROWSERBASE_PROJECT_ID: "project-id",
});
expect(buildStagehandFacadeEnv("BROWSERBASE")).toEqual({
STAGEHAND_BROWSER: "browserbase",
STAGEHAND_MODEL_API_KEY: "model-secret",
BROWSERBASE_PROJECT_ID: "project-id",
});
});

it("is supported by both agent harnesses with tool-owned startup profiles", () => {
expect(resolveClaudeCodeToolSurface("stagehand_facade")).toBe("stagehand_facade");
expect(resolveClaudeCodeStartupProfile("stagehand_facade", "LOCAL")).toBe("tool_launch_local");
expect(resolveClaudeCodeStartupProfile("stagehand_facade", "BROWSERBASE")).toBe(
"tool_create_browserbase",
);
expect(resolveCodexToolSurface("stagehand_facade")).toBe("stagehand_facade");
expect(resolveCodexStartupProfile("stagehand_facade", "LOCAL")).toBe("tool_launch_local");
expect(resolveCodexStartupProfile("stagehand_facade", "BROWSERBASE")).toBe(
"tool_create_browserbase",
);
});
});
Loading
Loading