Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
79aa866
feat(integrations): facade run executes the Playwright batch surface
miguelg719 Aug 31, 2026
3341bd6
feat(evals): stagehand_facade is the Playwright batch surface; add st…
miguelg719 Aug 31, 2026
8cb2314
fix(evals): default the rubric verifier to a live judge model
miguelg719 Aug 31, 2026
3cfd185
bench(hardbenchmark): validity audit — quarantine 1 bot-walled task, …
miguelg719 Aug 31, 2026
876f940
fix(evals): fx harness never reached the Stagehand MCP tools
miguelg719 Aug 31, 2026
994e9bc
fix(integrations): stop mastra/pi sessions retaining request bodies a…
miguelg719 Aug 31, 2026
4fb8a0f
fix(deepagents): keep provider tool-call blocks out of assistant text
miguelg719 Aug 31, 2026
e0bc551
fix(evals): keep the run summary alive when a Braintrust row has no o…
miguelg719 Aug 31, 2026
9ffb9c3
style(pi-sdk): format session test
miguelg719 Aug 31, 2026
d0d1be9
fix(evals): codex harness reaches runner-mounted MCP tools; count fac…
miguelg719 Aug 31, 2026
16e34f3
feat(integrations): facade page.frameLocator; clearer strict-mode and…
miguelg719 Aug 31, 2026
be44128
feat(evals): flag passes that never called the mounted browser surface
miguelg719 Aug 31, 2026
e123685
test(evals): pin default_tools_approval_mode on runner-mounted codex …
miguelg719 Aug 31, 2026
f8456cb
style(evals): oxfmt audit script and artifacts
miguelg719 Aug 31, 2026
9b3d571
fix(integrations): frame-scoped getAttribute fails with guidance
miguelg719 Aug 31, 2026
68c5f45
feat(evals): experiment metadata always carries tool_surface, model, …
miguelg719 Aug 31, 2026
f0cffe3
feat(integrations): demote raw SDK event logs to debug, drop stream d…
miguelg719 Aug 31, 2026
2695dd6
feat(evals): resolve the facade Browserbase session before the agent …
miguelg719 Aug 31, 2026
ff8a73a
feat(evals): shared readable step trace for every external harness
miguelg719 Aug 31, 2026
d307b1a
feat(evals): log the Browserbase session URL as the first line of eve…
miguelg719 Aug 31, 2026
e37d961
feat(evals): trace the agent's summary and final answer
miguelg719 Aug 31, 2026
8d5470b
fix(pi): keep pi's stock system prompt and default reasoning on
miguelg719 Aug 31, 2026
35b7a52
bench(hardbenchmark): quarantine Macy's (bot-wall), imgur (session de…
miguelg719 Aug 31, 2026
21c647e
fix(fx): final answer is the agent's conclusion, not its narration
miguelg719 Aug 31, 2026
2c22cc8
fix(evals): deterministic outcome gates, strict process score, answer…
miguelg719 Aug 31, 2026
1567a31
fix(sdk-ts): client-side deadline for callback batches
miguelg719 Aug 31, 2026
6beccf6
fix(facade): terminal browser-session-lost error instead of dead-sess…
miguelg719 Aug 31, 2026
6f63372
fix(evals): propagate browser_session_lost stop reason
miguelg719 Aug 31, 2026
813111b
fix(facade): explicit 1 h Browserbase session timeout, no keep-alive
miguelg719 Aug 31, 2026
ab063f5
fix(evals): only a trailing report parses without a marker
miguelg719 Aug 31, 2026
62c2928
feat(harnesses): request and capture model reasoning summaries
miguelg719 Aug 31, 2026
e264b0d
style(evals): oxfmt AUDIT-REVIEW.md
miguelg719 Aug 31, 2026
977e7b4
feat(eve): route non-first-party creators through the Vercel AI Gateway
miguelg719 Aug 31, 2026
22a0a87
fix(eve): pin a context window for gateway models missing from eve's …
miguelg719 Aug 31, 2026
bc85928
fix(evals): gated rows name the gate in the row error
miguelg719 Aug 31, 2026
98e1812
fix(evals): answer grounding is advisory by default
miguelg719 Aug 31, 2026
0a3ce86
feat(evals): dataset-aware step budgets (HardBench defaults to 75)
miguelg719 Aug 31, 2026
247e051
fix(eve): final answer and reasoning pairing
miguelg719 Aug 31, 2026
1d3f6ec
feat(evals): record terminationReason on persisted trajectories
miguelg719 Aug 31, 2026
4410556
fix(evals): persist gated outcome in scores/result.json
miguelg719 Aug 31, 2026
a692ce5
feat(evals): split agent vs verifier wall-clock
miguelg719 Aug 31, 2026
71dd3c9
feat(evals): normalized token usage across harnesses
miguelg719 Aug 31, 2026
63ce610
feat(evals): estimated cost from normalized usage and a versioned pri…
miguelg719 Aug 31, 2026
4c5c977
test(facade): deterministic layout-error retry window
miguelg719 Aug 31, 2026
e27975b
bench(hardbenchmark): require on-site evidence for constraint criteri…
miguelg719 Aug 31, 2026
2acb9ab
fix(facade): proxied + verified Browserbase sessions by default in ev…
miguelg719 Aug 31, 2026
c337309
test(codex-sdk): usage recovered from the CODEX_HOME rollout on budge…
miguelg719 Aug 31, 2026
3304ad4
fix(codex-sdk): keep token usage when the step budget aborts the turn
miguelg719 Aug 31, 2026
745e229
feat(evals): billed cost per row (reported, else computed at provider…
miguelg719 Aug 31, 2026
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
14 changes: 14 additions & 0 deletions packages/evals/core/contracts/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ToolSurface =
| "playwright_mcp"
| "chrome_devtools_mcp"
| "stagehand_facade"
| "stagehand_facade_legacy"
| "browse_cli";

export type StartupProfile =
Expand Down Expand Up @@ -135,6 +136,14 @@ export interface ToolStartInput {
};
}

/** Why a tool surface's browser became unusable mid-run. */
export interface BrowserSessionLoss {
cause: string;
/** Tool call that first observed the loss. */
tool?: string;
at?: string;
}

export interface ToolStartResult {
session: CoreSession;
/**
Expand All @@ -149,6 +158,11 @@ export interface ToolStartResult {
* Implementations must swallow per-field failures and must not throw.
*/
captureEvidence?: () => Promise<ProbeEvidence>;
/**
* Set once the surface's browser is gone for the rest of the run. Harnesses
* read it after the agent finishes to classify the outcome.
*/
browserSessionLoss?: () => BrowserSessionLoss | undefined;
/** Releases the runtime; `captureEvidence` is invalid after this resolves. */
cleanup: () => Promise<void>;
metadata: {
Expand Down
39 changes: 39 additions & 0 deletions packages/evals/core/tools/browserSessionLoss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { BrowserSessionLoss } from "../contracts/tool.js";

/**
* Wire strings the facade stdio server emits when its browser session is gone.
* Source of truth: packages/integrations/core/src/facade/contract.ts
* (BROWSER_SESSION_LOST_ERROR_PREFIX, SESSION_LOST_TELEMETRY_PREFIX). Mirrored
* here because evals consumes the integrations package through its built dist,
* and a facade build is allowed to lag behind the runner.
*/
export const BROWSER_SESSION_LOST_ERROR_PREFIX = "Browser session lost (";
export const SESSION_LOST_TELEMETRY_PREFIX = "stagehand_facade_session_lost ";

export function isBrowserSessionLostError(message: string): boolean {
return message.startsWith(BROWSER_SESSION_LOST_ERROR_PREFIX);
}

/** Extracts the cause from "Browser session lost (<cause>). ..." */
export function browserSessionLostCause(message: string): string | undefined {
if (!isBrowserSessionLostError(message)) return undefined;
return /^Browser session lost \((.*?)\)\./u.exec(message)?.[1] ?? message;
}

export function parseSessionLossTelemetry(line: string): BrowserSessionLoss | undefined {
if (!line.startsWith(SESSION_LOST_TELEMETRY_PREFIX)) return undefined;
try {
const parsed = JSON.parse(line.slice(SESSION_LOST_TELEMETRY_PREFIX.length)) as Record<
string,
unknown
>;
if (typeof parsed.cause !== "string") return undefined;
return {
cause: parsed.cause,
...(typeof parsed.tool === "string" && { tool: parsed.tool }),
...(typeof parsed.at === "string" && { at: parsed.at }),
};
} catch {
return undefined;
}
}
6 changes: 5 additions & 1 deletion packages/evals/core/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ 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 { StagehandFacadeLegacyTool, StagehandFacadeTool } from "./stagehand_facade.js";
import { UnderstudyCodeTool } from "./understudy_code.js";

/** Surfaces that exist only as an agent MCP mount; they have no runner-driven CoreSession (activePage() throws). */
export const AGENT_MOUNT_ONLY_TOOL_SURFACES: ReadonlySet<ToolSurface> = new Set<ToolSurface>([
"stagehand_facade",
"stagehand_facade_legacy",
]);

export function isAgentMountOnlyToolSurface(toolSurface: ToolSurface): boolean {
Expand All @@ -28,6 +29,7 @@ export function listCoreTools(): ToolSurface[] {
// Listed here as part of the full enumeration, but agent-mount-only:
// core-tier selection must use listCoreRunnableTools, which filters it.
"stagehand_facade",
"stagehand_facade_legacy",
"browse_cli",
];
}
Expand All @@ -53,6 +55,8 @@ export function getCoreTool(toolSurface: ToolSurface): CoreTool {
return new ChromeDevtoolsMcpTool();
case "stagehand_facade":
return new StagehandFacadeTool();
case "stagehand_facade_legacy":
return new StagehandFacadeLegacyTool();
case "browse_cli":
return new BrowseCliTool();
default:
Expand Down
82 changes: 76 additions & 6 deletions packages/evals/core/tools/stagehandFacadeBridge.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { spawn } from "node:child_process";
import net, { type AddressInfo, type Socket } from "node:net";
import type { ProbeEvidence } from "stagehand-v3";
import { SESSION_INFO_TOOL_NAME } from "@browserbasehq/stagehand-integrations/facade";
import type { BrowserSessionLoss } from "../contracts/tool.js";
import { browserSessionLostCause, parseSessionLossTelemetry } from "./browserSessionLoss.js";
import { sanitizeErrorMessage } from "@browserbasehq/stagehand-integrations/harness";
import type { EvalLogger } from "../../logger.js";

export const STAGEHAND_FACADE_BRIDGE_PORT_ENV = "STAGEHAND_EVALS_FACADE_BRIDGE_PORT";

const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
const SESSION_INFO_TIMEOUT_MS = 60_000;
const HOST_ENV_KEYS = [
"PATH",
"HOME",
Expand All @@ -29,6 +33,11 @@ export interface StagehandFacadeBridgeInput {
requestTimeoutMs?: number;
}

export interface FacadeBrowserSessionInfo {
provider: "browserbase" | "local";
sessionId?: string;
}

export interface StagehandFacadeBridge {
/** Loopback port the relay connects to. */
port: number;
Expand All @@ -38,10 +47,25 @@ export interface StagehandFacadeBridge {
agentConnections(): number;
/** Whether any agent tools/call request has passed through. */
sawAgentToolCall(): boolean;
/**
* Set once the facade reported its browser session gone. Every tool call
* after that returns the same terminal error, so failures past this point
* are consequences of the loss, not agent mistakes.
*/
browserSessionLoss(): BrowserSessionLoss | undefined;
/** Raw JSON-RPC request from the runner side. */
call(method: string, params?: Record<string, unknown>): Promise<unknown>;
call(
method: string,
params?: Record<string, unknown>,
options?: { timeoutMs?: number },
): Promise<unknown>;
/** Best-effort terminal/step evidence from the shared browser. */
captureEvidence(): Promise<ProbeEvidence>;
/**
* Launches the facade browser if it has not started yet and reports where it
* lives. Runner-side only; the agent never sees this tool.
*/
sessionInfo(): Promise<FacadeBrowserSessionInfo>;
/** Idempotently closes the relay and facade process. */
close(): Promise<void>;
}
Expand Down Expand Up @@ -112,6 +136,12 @@ function isToolError(result: unknown): boolean {
return Boolean(result && typeof result === "object" && (result as { isError?: unknown }).isError);
}

function sessionLostCauseFromToolResult(result: unknown): string | undefined {
if (!isToolError(result)) return undefined;
const text = contentBlocks(result).find((block) => typeof block.text === "string")?.text;
return typeof text === "string" ? browserSessionLostCause(text) : undefined;
}

export async function startStagehandFacadeBridge(
input: StagehandFacadeBridgeInput,
): Promise<StagehandFacadeBridge> {
Expand Down Expand Up @@ -144,6 +174,13 @@ export async function startStagehandFacadeBridge(
let exitDescription = "";
let initializeResult: unknown;
let closePromise: Promise<void> | undefined;
let sessionLoss: BrowserSessionLoss | undefined;

const noteSessionLoss = (loss: BrowserSessionLoss) => {
if (sessionLoss) return;
sessionLoss = loss;
log(`Facade browser session lost: ${loss.cause}`);
};

const rejectPending = (error: Error) => {
for (const entry of pending.values()) {
Expand All @@ -158,6 +195,8 @@ export async function startStagehandFacadeBridge(
stderrLines.push(line);
if (stderrLines.length > 20) stderrLines.shift();
log(line);
const loss = parseSessionLossTelemetry(line);
if (loss) noteSessionLoss(loss);
};

child.stderr.on("data", (chunk: Buffer | string) => {
Expand Down Expand Up @@ -213,6 +252,10 @@ export async function startStagehandFacadeBridge(
return;
}

// The stderr telemetry line is the primary signal; the terminal tool error
// covers a facade build that predates it.
const lostCause = sessionLostCauseFromToolResult(message.result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a legacy facade reports the terminal session-loss tool error only to a runner-issued request, this check never runs because the evals-facade-* response path returns first. Parse terminal tool errors before that early return, or the runner can grade a browser-dead run as a normal agent outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/core/tools/stagehandFacadeBridge.ts, line 257:

<comment>When a legacy facade reports the terminal session-loss tool error only to a runner-issued request, this check never runs because the `evals-facade-*` response path returns first. Parse terminal tool errors before that early return, or the runner can grade a browser-dead run as a normal agent outcome.</comment>

<file context>
@@ -229,6 +252,10 @@ export async function startStagehandFacadeBridge(
 
+    // The stderr telemetry line is the primary signal; the terminal tool error
+    // covers a facade build that predates it.
+    const lostCause = sessionLostCauseFromToolResult(message.result);
+    if (lostCause) noteSessionLoss({ cause: lostCause });
     const socket = agentRequests.get(idKey(message.id));
</file context>

if (lostCause) noteSessionLoss({ cause: lostCause });
const socket = agentRequests.get(idKey(message.id));
agentRequests.delete(idKey(message.id));
if (socket?.writable) socket.write(`${rawLine}\n`);
Expand Down Expand Up @@ -319,7 +362,12 @@ export async function startStagehandFacadeBridge(
}
const port = (server.address() as AddressInfo).port;

const call = (method: string, params?: Record<string, unknown>): Promise<unknown> => {
const call = (
method: string,
params?: Record<string, unknown>,
options: { timeoutMs?: number } = {},
): Promise<unknown> => {
const timeoutMs = options.timeoutMs ?? requestTimeoutMs;
if (closed) return Promise.reject(new Error("Stagehand facade bridge is closed"));
if (exited) {
return Promise.reject(
Expand All @@ -330,10 +378,8 @@ export async function startStagehandFacadeBridge(
return new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(
new Error(`Stagehand facade request "${method}" timed out after ${requestTimeoutMs}ms`),
);
}, requestTimeoutMs);
reject(new Error(`Stagehand facade request "${method}" timed out after ${timeoutMs}ms`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
const request = {
jsonrpc: "2.0",
Expand Down Expand Up @@ -401,6 +447,28 @@ export async function startStagehandFacadeBridge(
return evidence;
};

const sessionInfo = async (): Promise<FacadeBrowserSessionInfo> => {
// A cold Browserbase launch can outlast the per-request default.
const result = await call(
"tools/call",
{ name: SESSION_INFO_TOOL_NAME, arguments: {} },
{ timeoutMs: Math.max(requestTimeoutMs, SESSION_INFO_TIMEOUT_MS) },
);
if (isToolError(result)) {
const text = contentBlocks(result).find((block) => typeof block.text === "string")?.text;
throw new Error(sanitizeErrorMessage(`Stagehand facade session_info failed: ${text ?? ""}`));
}
const text = contentBlocks(result).find(
(block) => block.type === "text" && typeof block.text === "string",
)?.text;
const parsed = JSON.parse(String(text ?? "{}")) as Record<string, unknown>;
return {
provider: parsed.provider === "browserbase" ? "browserbase" : "local",
...(typeof parsed.sessionId === "string" &&
parsed.sessionId && { sessionId: parsed.sessionId }),
};
};

const close = (): Promise<void> => {
closePromise ??= (async () => {
closed = true;
Expand Down Expand Up @@ -431,8 +499,10 @@ export async function startStagehandFacadeBridge(
},
agentConnections: () => sockets.size,
sawAgentToolCall: () => toolCallSeen,
browserSessionLoss: () => sessionLoss,
call,
captureEvidence,
sessionInfo,
close,
};

Expand Down
4 changes: 3 additions & 1 deletion packages/evals/core/tools/stagehand_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ export class StagehandCodeTool implements CoreTool {
input.logger.log({
category: "stagehand_code",
message: "Initialized stagehand_code Stagehand SDK runtime.",
level: 1,
level: 2,
auxiliary: {
startupProfile: { value: input.startupProfile, type: "string" },
environment: { value: input.environment, type: "string" },
Expand Down Expand Up @@ -496,6 +496,8 @@ export class StagehandCodeTool implements CoreTool {
browserOwnership: "tool",
connectionMode: connectionModeFromProfile(input.startupProfile),
startupProfile: input.startupProfile,
...(sdk.sessionUrl && { browserbaseSessionUrl: sdk.sessionUrl }),
...(sdk.debugUrl && { browserbaseDebugUrl: sdk.debugUrl }),
},
};
}
Expand Down
Loading
Loading