Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9c33b70
add p2p stagehand support for canonicalization rpc
Nov 22, 2025
6923a4d
tweaks to fix sse for python clients
Nov 22, 2025
f97e678
fix python client
Nov 25, 2025
0650606
centralize v3 stagehand-api schema and generate openapi spec from zod…
Nov 25, 2025
e72a888
use eventBus for LLM request and responses as well
Nov 25, 2025
dc25e8b
use event bus for llm events
Dec 1, 2025
3c61ca3
fix json schema to zod conversion
pirate Dec 2, 2025
854fe57
eliminate branching in remote vs p2p connection process
pirate Dec 3, 2025
920ec70
wip generic SessionStore interface
pirate Dec 3, 2025
501fcd4
implement SessionStore interface for cloud to hook into
pirate Dec 3, 2025
a4ff394
cleanup schemas
pirate Dec 3, 2025
68e5779
remove unused events
pirate Dec 3, 2025
40312bf
fix type errors in library p2p server
pirate Dec 3, 2025
eb76ea3
add missing type export
pirate Dec 3, 2025
e587d8e
add fastify-type-provider-zod to validate request/response shapes bef…
pirate Dec 3, 2025
5263dce
better naming
pirate Dec 3, 2025
8ef155c
a few stragglers for names
pirate Dec 3, 2025
aaab1a3
fix sessionstartnaming
pirate Dec 3, 2025
db4045c
remove dead integration test
pirate Dec 3, 2025
c0a1a7d
type fixes
monadoid Dec 3, 2025
7aec8ca
remove python examples for now
pirate Dec 3, 2025
aefb1b4
Merge remote-tracking branch 'origin/stagehand-p2p' into stagehand-p2p
monadoid Dec 3, 2025
382f6f2
added tests (not working yet)
monadoid Dec 4, 2025
dfcb1de
use generics to pass down inferred req and resp strict types
pirate Dec 4, 2025
1227a2f
type the End request as well
pirate Dec 4, 2025
b1703a4
cleanup session validation precheck
pirate Dec 4, 2025
c83c013
Fixed params in vitest
monadoid Dec 4, 2025
cc14f60
properly handle errors in library stream
pirate Dec 4, 2025
3c94946
properly type the stagehand obj
pirate Dec 4, 2025
d994118
remove unused imports and codeformat
pirate Dec 4, 2025
569ac1e
Fixed env vars
monadoid Dec 4, 2025
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: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"dev": "turbo run dev",
"example": "pnpm --filter @browserbasehq/stagehand run example --",
"cache:clear": "turbo run build --force",
"prepare": "turbo run build",
"release": "turbo run build && changeset publish",
"release-canary": "turbo run build && changeset version --snapshot && changeset publish --tag alpha"
},
Expand Down
41 changes: 26 additions & 15 deletions packages/core/lib/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from "./prompt";
import { appendSummary, writeTimestampedTxtFile } from "./inferenceLogUtils";
import type { InferStagehandSchema, StagehandZodObject } from "./v3/zodCompat";
import { createChatCompletionViaEventBus } from "./v3/llm/llmEventBridge";
import type { StagehandEventBus } from "./v3/eventBus";

// Re-export for backward compatibility
export type { LLMParsedResponse, LLMUsage } from "./v3/llm/LLMClient";
Expand All @@ -21,6 +23,7 @@ export async function extract<T extends StagehandZodObject>({
domElements,
schema,
llmClient,
eventBus,
logger,
userProvidedInstructions,
logInferenceToFile = false,
Expand All @@ -29,6 +32,7 @@ export async function extract<T extends StagehandZodObject>({
domElements: string;
schema: T;
llmClient: LLMClient;
eventBus: StagehandEventBus;
userProvidedInstructions?: string;
logger: (message: LogLine) => void;
logInferenceToFile?: boolean;
Expand Down Expand Up @@ -74,7 +78,7 @@ export async function extract<T extends StagehandZodObject>({

const extractStartTime = Date.now();
const extractionResponse =
await llmClient.createChatCompletion<ExtractionResponse>({
await createChatCompletionViaEventBus<ExtractionResponse>(eventBus, {
options: {
messages: extractCallMessages,
response_model: {
Expand Down Expand Up @@ -139,7 +143,7 @@ export async function extract<T extends StagehandZodObject>({

const metadataStartTime = Date.now();
const metadataResponse =
await llmClient.createChatCompletion<MetadataResponse>({
await createChatCompletionViaEventBus<MetadataResponse>(eventBus, {
options: {
messages: metadataCallMessages,
response_model: {
Expand Down Expand Up @@ -224,13 +228,15 @@ export async function observe({
instruction,
domElements,
llmClient,
eventBus,
userProvidedInstructions,
logger,
logInferenceToFile = false,
}: {
instruction: string;
domElements: string;
llmClient: LLMClient;
eventBus: StagehandEventBus;
userProvidedInstructions?: string;
logger: (message: LogLine) => void;
logInferenceToFile?: boolean;
Expand Down Expand Up @@ -291,20 +297,23 @@ export async function observe({
}

const start = Date.now();
const rawResponse = await llmClient.createChatCompletion<ObserveResponse>({
options: {
messages,
response_model: {
schema: observeSchema,
name: "Observation",
const rawResponse = await createChatCompletionViaEventBus<ObserveResponse>(
eventBus,
{
options: {
messages,
response_model: {
schema: observeSchema,
name: "Observation",
},
temperature: isGPT5 ? 1 : 0.1,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
},
temperature: isGPT5 ? 1 : 0.1,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
logger,
},
logger,
});
);
const end = Date.now();
const usageTimeMs = end - start;

Expand Down Expand Up @@ -364,13 +373,15 @@ export async function act({
instruction,
domElements,
llmClient,
eventBus,
userProvidedInstructions,
logger,
logInferenceToFile = false,
}: {
instruction: string;
domElements: string;
llmClient: LLMClient;
eventBus: StagehandEventBus;
userProvidedInstructions?: string;
logger: (message: LogLine) => void;
logInferenceToFile?: boolean;
Expand Down Expand Up @@ -424,7 +435,7 @@ export async function act({
}

const start = Date.now();
const rawResponse = await llmClient.createChatCompletion<ActResponse>({
const rawResponse = await createChatCompletionViaEventBus<ActResponse>(eventBus, {
options: {
messages,
response_model: {
Expand Down
69 changes: 49 additions & 20 deletions packages/core/lib/v3/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ExecuteActionParams,
StagehandAPIConstructorParams,
StartSessionParams,
StartSessionResult,
SessionStartResult,
} from "./types/private";
import {
ActResult,
Expand Down Expand Up @@ -54,17 +54,42 @@ interface ReplayMetricsResponse {
}

export class StagehandAPIClient {
private apiKey: string;
private projectId: string;
private apiKey?: string;
private projectId?: string;
private sessionId?: string;
private modelApiKey: string;
private modelApiKey?: string;
private baseUrl: string;
private logger: (message: LogLine) => void;
private fetchWithCookies;

constructor({ apiKey, projectId, logger }: StagehandAPIConstructorParams) {
constructor({
apiKey,
projectId,
baseUrl,
logger,
}: StagehandAPIConstructorParams) {
this.apiKey = apiKey;
this.projectId = projectId;
const resolvedBaseUrl =
baseUrl ||
process.env.STAGEHAND_API_URL ||
"https://api.stagehand.browserbase.com/v1";
this.baseUrl = resolvedBaseUrl;
this.logger = logger;

// Validate: if using the default cloud API (no explicit override),
// apiKey and projectId are required. When STAGEHAND_API_URL or an
// explicit baseUrl is provided, we allow missing keys so the same
// client can talk to both cloud and P2P servers.
const usingDefaultCloud =
!baseUrl && !process.env.STAGEHAND_API_URL;
if (usingDefaultCloud && (!apiKey || !projectId)) {
throw new StagehandAPIError(
"apiKey and projectId are required when using the cloud API. " +
"Set STAGEHAND_API_URL or provide a baseUrl to connect to a local Stagehand server instead.",
);
}

// Create a single cookie jar instance that will persist across all requests
this.fetchWithCookies = makeFetchCookie(fetch);
}
Expand All @@ -78,7 +103,7 @@ export class StagehandAPIClient {
selfHeal,
browserbaseSessionCreateParams,
browserbaseSessionID,
}: StartSessionParams): Promise<StartSessionResult> {
}: StartSessionParams): Promise<SessionStartResult> {
if (!modelApiKey) {
throw new StagehandAPIError("modelApiKey is required");
}
Expand Down Expand Up @@ -121,7 +146,7 @@ export class StagehandAPIClient {
}

const sessionResponseBody =
(await sessionResponse.json()) as ApiResponse<StartSessionResult>;
(await sessionResponse.json()) as ApiResponse<SessionStartResult>;

if (sessionResponseBody.success === false) {
throw new StagehandAPIError(sessionResponseBody.message);
Expand Down Expand Up @@ -477,30 +502,34 @@ export class StagehandAPIClient {

private async request(path: string, options: RequestInit): Promise<Response> {
const defaultHeaders: Record<string, string> = {
"x-bb-api-key": this.apiKey,
"x-bb-project-id": this.projectId,
"x-bb-session-id": this.sessionId,
// we want real-time logs, so we stream the response
"x-stream-response": "true",
"x-model-api-key": this.modelApiKey,
"x-sent-at": new Date().toISOString(),
"x-language": "typescript",
"x-sdk-version": STAGEHAND_VERSION,
};

// Only add auth headers if they exist (cloud mode)
// Always send auth-related headers; servers that don't require them
// (e.g. P2P or self-hosted) will simply ignore empty values.
defaultHeaders["x-bb-api-key"] = this.apiKey ?? "";
defaultHeaders["x-bb-project-id"] = this.projectId ?? "";
if (this.sessionId) {
defaultHeaders["x-bb-session-id"] = this.sessionId;
}
defaultHeaders["x-model-api-key"] = this.modelApiKey ?? "";

if (options.method === "POST" && options.body) {
defaultHeaders["Content-Type"] = "application/json";
}

const response = await this.fetchWithCookies(
`${process.env.STAGEHAND_API_URL ?? "https://api.stagehand.browserbase.com/v1"}${path}`,
{
...options,
headers: {
...defaultHeaders,
...options.headers,
},
const response = await this.fetchWithCookies(`${this.baseUrl}${path}`, {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
);
});

return response;
}
Expand Down
64 changes: 64 additions & 0 deletions packages/core/lib/v3/eventBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Central Event Bus for Stagehand
*
* Single event emitter shared by:
* - V3 class (LLM events, library events)
* - StagehandServer (server lifecycle, request/response events)
* - External listeners (cloud servers, monitoring, etc.)
*/

import { EventEmitter } from "events";
import type { StagehandServerEventMap } from "./server/events";

/**
* Type-safe event bus for all Stagehand events
*/
export class StagehandEventBus extends EventEmitter {
/**
* Emit an event and wait for all async listeners to complete
*/
async emitAsync<K extends keyof StagehandServerEventMap>(
event: K,
data: StagehandServerEventMap[K],
): Promise<void> {
const listeners = this.listeners(event);
await Promise.all(listeners.map((listener) => listener(data)));
}

/**
* Type-safe event listener
*/
on<K extends keyof StagehandServerEventMap>(
event: K,
listener: (data: StagehandServerEventMap[K]) => void | Promise<void>,
): this {
return super.on(event, listener);
}

/**
* Type-safe one-time event listener
*/
once<K extends keyof StagehandServerEventMap>(
event: K,
listener: (data: StagehandServerEventMap[K]) => void | Promise<void>,
): this {
return super.once(event, listener);
}

/**
* Type-safe remove listener
*/
off<K extends keyof StagehandServerEventMap>(
event: K,
listener: (data: StagehandServerEventMap[K]) => void | Promise<void>,
): this {
return super.off(event, listener);
}
}

/**
* Create a new event bus instance
*/
export function createEventBus(): StagehandEventBus {
return new StagehandEventBus();
}
7 changes: 7 additions & 0 deletions packages/core/lib/v3/handlers/actHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
performUnderstudyMethod,
waitForDomNetworkQuiet,
} from "./handlerUtils/actHandlerUtils";
import type { StagehandEventBus } from "../eventBus";

export class ActHandler {
private readonly llmClient: LLMClient;
Expand All @@ -40,12 +41,14 @@ export class ActHandler {
inferenceTimeMs: number,
) => void;
private readonly defaultDomSettleTimeoutMs?: number;
private readonly eventBus: StagehandEventBus;

constructor(
llmClient: LLMClient,
defaultModelName: AvailableModel,
defaultClientOptions: ClientOptions,
resolveLlmClient: (model?: ModelConfiguration) => LLMClient,
eventBus: StagehandEventBus,
systemPrompt?: string,
logInferenceToFile?: boolean,
selfHeal?: boolean,
Expand All @@ -63,6 +66,7 @@ export class ActHandler {
this.defaultModelName = defaultModelName;
this.defaultClientOptions = defaultClientOptions;
this.resolveLlmClient = resolveLlmClient;
this.eventBus = eventBus;
this.systemPrompt = systemPrompt ?? "";
this.logInferenceToFile = logInferenceToFile ?? false;
this.selfHeal = !!selfHeal;
Expand Down Expand Up @@ -100,6 +104,7 @@ export class ActHandler {
instruction: observeActInstruction,
domElements: combinedTree,
llmClient,
eventBus: this.eventBus,
userProvidedInstructions: this.systemPrompt,
logger: v3Logger,
logInferenceToFile: this.logInferenceToFile,
Expand Down Expand Up @@ -230,6 +235,7 @@ export class ActHandler {
instruction: stepTwoInstructions,
domElements: diffedTree,
llmClient,
eventBus: this.eventBus,
userProvidedInstructions: this.systemPrompt,
logger: v3Logger,
logInferenceToFile: this.logInferenceToFile,
Expand Down Expand Up @@ -422,6 +428,7 @@ export class ActHandler {
instruction,
domElements: combinedTree,
llmClient: effectiveClient,
eventBus: this.eventBus,
userProvidedInstructions: this.systemPrompt,
logger: v3Logger,
logInferenceToFile: this.logInferenceToFile,
Expand Down
Loading