Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e7aa685
Add Claude native browser use through the shared facade runtime
miguelg719 Sep 7, 2026
56052db
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
cdac6bd
Preserve Claude CUA screenshot events during history compression
miguelg719 Sep 8, 2026
25d618f
Execute CUA drags through the supported SDK gesture
miguelg719 Sep 8, 2026
16e4b70
Honor Claude CUA target tabs and reject unsupported click modifiers
miguelg719 Sep 8, 2026
4200e2c
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
49e0753
Format Claude CUA capability documentation
miguelg719 Sep 8, 2026
0dc9903
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
a7eb902
Wire trusted session-loss state into Claude CUA
miguelg719 Sep 8, 2026
cf2f281
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
f1fda18
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
ba54edf
style(evals): sort Claude CUA workspace dependency
miguelg719 Sep 8, 2026
ce59574
fix(claude-cua): preserve terminal ownership and visible facade tab s…
miguelg719 Sep 8, 2026
061df40
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
32a7a95
test(claude-cua): validate canonical visible tab contract
miguelg719 Sep 8, 2026
a534540
Merge branch 'evals/consolidation-15-shared-cua-bridge' into evals/co…
miguelg719 Sep 8, 2026
6ee584b
Merge commit '1fea5a53b88e6a2a3302654f26e1aa84956a806d' into HEAD
miguelg719 Sep 10, 2026
6cb6560
Merge commit 'b4bdb00dbd25c187d6cc0cf5facea8574ce97358' into HEAD
miguelg719 Sep 10, 2026
fc53cee
Merge commit '634952089142e8ede581b9b5e3908f01d6a06039' into HEAD
miguelg719 Sep 10, 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ jobs:
packages/sdk-ts/dist/**
packages/integrations/core/dist/**
packages/integrations/claude-agent-sdk/dist/**
packages/integrations/claude-cua-sdk/dist/**
packages/integrations/codex-sdk/dist/**
packages/integrations/mastra-sdk/dist/**
packages/integrations/pi-sdk/dist/**
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"
| "anthropic_browser_toolset"
| "stagehand_facade"
| "stagehand_facade_legacy"
| "browse_cli";
Expand Down
10 changes: 9 additions & 1 deletion packages/evals/core/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ 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, StagehandFacadeLegacyTool } from "./stagehand_facade.js";
import {
StagehandFacadeTool,
StagehandFacadeLegacyTool,
AnthropicBrowserToolsetTool,
} 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>([
"anthropic_browser_toolset",
"stagehand_facade",
"stagehand_facade_legacy",
]);
Expand All @@ -28,6 +33,7 @@ export function listCoreTools(): ToolSurface[] {
"chrome_devtools_mcp",
// Listed here as part of the full enumeration, but agent-mount-only:
// core-tier selection must use listCoreRunnableTools, which filters it.
"anthropic_browser_toolset",
"stagehand_facade",
"stagehand_facade_legacy",
"browse_cli",
Expand All @@ -53,6 +59,8 @@ export function getCoreTool(toolSurface: ToolSurface): CoreTool {
return new PlaywrightMcpTool();
case "chrome_devtools_mcp":
return new ChromeDevtoolsMcpTool();
case "anthropic_browser_toolset":
return new AnthropicBrowserToolsetTool();
case "stagehand_facade":
return new StagehandFacadeTool();
case "stagehand_facade_legacy":
Expand Down
5 changes: 5 additions & 0 deletions packages/evals/core/tools/stagehand_facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,8 @@ export class StagehandFacadeLegacyTool extends StagehandFacadeTool {
return LEGACY_FACADE_AGENT_INSTRUCTIONS;
}
}

/** Native Claude members execute on the same facade-owned browser. */
export class AnthropicBrowserToolsetTool extends StagehandFacadeTool {
override readonly id: ToolSurface = "anthropic_browser_toolset";
}
11 changes: 11 additions & 0 deletions packages/evals/framework/benchHarness.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { runClaudeCuaAgent, CLAUDE_CUA_DEFAULT_MODELS } from "./claudeCuaRunner.js";
import { CLAUDE_CUA_TOOL_SURFACES, prepareClaudeCuaToolAdapter } from "./claudeCuaToolAdapter.js";
import { V3, normalizeRubric, type AvailableModel, type TaskSpec } from "stagehand-v3";
import { EvalsError } from "../errors.js";
import { sanitizeErrorMessage } from "@browserbasehq/stagehand-integrations/harness";
Expand Down Expand Up @@ -379,6 +381,14 @@ export const cursorHarness = defineExternalHarness({
runAgent: runCursorAgent,
});

export const claudeCuaHarness = defineExternalHarness({
harness: "claude_cua",
supportedToolSurfaces: CLAUDE_CUA_TOOL_SURFACES,
defaultModels: CLAUDE_CUA_DEFAULT_MODELS,
prepareToolAdapter: prepareClaudeCuaToolAdapter,
runAgent: runClaudeCuaAgent,
});

const harnessRegistry = new Map<Harness, BenchHarness>([
["stagehand", stagehandHarness],
["claude_code", claudeCodeHarness],
Expand All @@ -389,6 +399,7 @@ const harnessRegistry = new Map<Harness, BenchHarness>([
["deepagents", deepagentsHarness],
["fx", fxHarness],
["cursor", cursorHarness],
["claude_cua", claudeCuaHarness],
]);

export function registerBenchHarness(harness: BenchHarness): () => void {
Expand Down
191 changes: 191 additions & 0 deletions packages/evals/framework/claudeCuaRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
buildClaudeCuaTranscript,
runClaudeCuaSession,
stringifyError,
type CuaMessagesClient,
type CuaThinkingConfig,
} from "@browserbasehq/stagehand-integrations-claude-cua-sdk";
import type { AvailableModel } from "stagehand-v3";
import { EvalsError } from "../errors.js";
import type { EvalLogger } from "../logger.js";
import type { PreparedClaudeCuaToolAdapter } from "./claudeCuaToolAdapter.js";
import type { ExternalHarnessTaskPlan } from "./externalHarnessPlan.js";
import { claudeCuaAdapter } from "./harnesses/claudeCuaAdapter.js";
import {
buildExternalHarnessPrompt,
metricValue,
runExternalHarnessTask,
} from "./harnesses/externalRunner.js";
import { resolveStepBudget } from "./stepBudget.js";
import type { TaskResult } from "./types.js";
import type { ExternalHarnessVerifierConfig } from "./verifierAdapter.js";

/**
* Models the harness plans when --model is omitted. The planner honors
* EVAL_CLAUDE_CUA_MODELS (comma separated) over this list, so the allowlist of
* toolset-capable checkpoints lives in the environment, not in code.
*
* `browser_toolset_20260801` is accepted by the Claude 5 family and Opus 4.8
* (CLAUDE_CUA_CAPABLE_MODELS); the API rejects it with a 400 on Claude 4.6 and
* older (verified live on claude-sonnet-4-6, 2026-08-31). The default is the
* cheapest capable model.
*/
export const CLAUDE_CUA_DEFAULT_MODELS: AvailableModel[] = [
"anthropic/claude-sonnet-5" as AvailableModel,
];

export const CLAUDE_CUA_CAPABLE_MODELS = [
"claude-opus-4-8",
"claude-opus-5",
"claude-sonnet-5",
"claude-fable-5",
"claude-fable-5-1",
] as const;

export const CLAUDE_CUA_SYSTEM_PROMPT =
"You are being evaluated on a browser task. Complete it with the browser tools only and finish by printing the requested EVAL_RESULT line.";

export interface ClaudeCuaRunnerInput {
plan: ExternalHarnessTaskPlan;
model: AvailableModel;
logger: EvalLogger;
toolAdapter: PreparedClaudeCuaToolAdapter;
signal?: AbortSignal;
verifier?: ExternalHarnessVerifierConfig;
/** Scripted Messages client for tests. */
client?: CuaMessagesClient;
}

export function buildClaudeCuaPrompt(
plan: ExternalHarnessTaskPlan,
toolInstructions?: string,
): string {
return buildExternalHarnessPrompt({ plan, toolInstructions, resultContract: "marker" });
}

/**
* The Browser Use toolset is an Anthropic tool surface, so a non-Anthropic
* provider is rejected. Claude models outside the public allowlist only warn:
* new checkpoints land faster than this list is updated.
*/
export function assertClaudeCuaModel(model: string, logger?: Pick<EvalLogger, "warn">): void {
const provider = model.includes("/") ? model.slice(0, model.indexOf("/")) : undefined;
const bare = provider ? model.slice(provider.length + 1) : model;
if ((provider !== undefined && provider !== "anthropic") || !bare.startsWith("claude-")) {
throw new EvalsError(
`claude_cua runs Anthropic Claude models only (received "${model}"). Set EVAL_CLAUDE_CUA_MODELS or pass -m anthropic/<model>.`,
);
}
if (!(CLAUDE_CUA_CAPABLE_MODELS as readonly string[]).includes(bare)) {
logger?.warn({
category: "claude_cua",
level: 1,
message: `Model "${model}" is outside the public Browser Use model allowlist (${CLAUDE_CUA_CAPABLE_MODELS.join(", ")}); continuing anyway.`,
});
}
}

/**
* Thinking is on (adaptive) unless EVAL_CLAUDE_CUA_THINKING=off.
* EVAL_CLAUDE_CUA_THINKING_EFFORT sets output_config.effort;
* EVAL_CLAUDE_CUA_THINKING_BUDGET switches to budget_tokens for pre-4.6 models.
*/
export function resolveClaudeCuaThinking(env: NodeJS.ProcessEnv = process.env): CuaThinkingConfig {
if ((env.EVAL_CLAUDE_CUA_THINKING ?? "").trim().toLowerCase() === "off") {
return { type: "disabled" };
}
const budget = Number.parseInt(env.EVAL_CLAUDE_CUA_THINKING_BUDGET ?? "", 10);
if (Number.isFinite(budget) && budget > 0) return { type: "enabled", budgetTokens: budget };
const effort = (env.EVAL_CLAUDE_CUA_THINKING_EFFORT ?? "").trim().toLowerCase();
const efforts = ["low", "medium", "high", "xhigh", "max"] as const;
return {
type: "adaptive",
effort: (efforts as readonly string[]).includes(effort)
? (effort as (typeof efforts)[number])
: "xhigh",
};
}

export async function runClaudeCuaAgent(input: ClaudeCuaRunnerInput): Promise<TaskResult> {
const { plan, model, logger, toolAdapter, signal, verifier, client } = input;
assertClaudeCuaModel(model, logger);
// One turn is one API round trip and may hold several tool members; the
// shared tool-step budget is a mild over-allowance, as for claude_code / pi.
const maxTurns = resolveStepBudget({
harnessEnvKey: "EVAL_CLAUDE_CUA_MAX_TURNS",
dataset: plan.dataset,
harnessDefault: 50,
});
const thinking = resolveClaudeCuaThinking();
return runExternalHarnessTask({
harness: "claude_cua",
plan,
model,
logger,
toolAdapter,
verifier,
resultContract: "marker",
fallbackErrorMessage: "claude_cua did not report success",
stepBudget: maxTurns,
stepBudgetUnit: "turns",
configuration: {
requestedThinking: thinking,
...(thinking.type === "adaptive" && { requestedReasoningEffort: thinking.effort }),
},
systemPromptMode: "native",
runSession: async (prompt, systemPrompt) => {
const sessionResult = await runClaudeCuaSession({
prompt,
model,
logger,
signal,
maxTurns,
tools: toolAdapter.executor,
systemPrompt: `${systemPrompt}\n\n${CLAUDE_CUA_SYSTEM_PROMPT}`,
thinking,
...(client && { client }),
});
const usage = sessionResult.tokenUsage;
return {
raw: sessionResult,
resultText: sessionResult.finalMessage,
transcriptText: buildClaudeCuaTranscript(sessionResult.events),
iterationError: sessionResult.iterationError,
status: sessionResult.status,
stopReason:
sessionResult.status === "sdk_error"
? sessionResult.stopReason || stringifyError(sessionResult.iterationError) || undefined
: sessionResult.stopReason,
// Anthropic shape: input excludes cache reads/writes (anthropic_cache_separate).
usage: {
reported: sessionResult.usageReported,
inputTokens: usage.input,
outputTokens: usage.output,
cachedInputTokens: usage.cache_read,
cacheCreationInputTokens: usage.cache_creation,
totalTokens: usage.total,
},
metrics: {
claude_cua_turns: metricValue(sessionResult.turns),
claude_cua_tool_calls: metricValue(sessionResult.toolCalls),
},
};
},
toTrajectory: ({ raw, parsed, finalObservation, status }, taskSpec) =>
claudeCuaAdapter.fromHarnessResult(
{
events: raw.events,
...(finalObservation && { finalObservation }),
stepObservations: toolAdapter.drainObservationsByToolUse(),
finalAnswer: parsed.finalAnswer ?? raw.finalMessage,
status,
usage: {
input_tokens: raw.tokenUsage.input,
output_tokens: raw.tokenUsage.output,
cached_input_tokens: raw.tokenUsage.cache_read,
},
},
taskSpec,
),
});
}
Loading
Loading