Skip to content

feat(Stagehand): fix startup crashes, improve Screenshot #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
51 changes: 40 additions & 11 deletions stagehand/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,18 @@ export const stagehandConfig: ConstructorParams = {
: undefined,
}
: undefined,
localBrowserLaunchOptions: process.env.LOCAL_CDP_URL
? {
cdpUrl: process.env.LOCAL_CDP_URL,
}
: undefined,
// Set a default CDP URL if running in LOCAL mode
localBrowserLaunchOptions: process.env.BROWSERBASE_API_KEY && process.env.BROWSERBASE_PROJECT_ID
? undefined
: {
cdpUrl: process.env.LOCAL_CDP_URL || "http://localhost:9222",
},
enableCaching: true /* Enable caching functionality */,
browserbaseSessionID:
undefined /* Session ID for resuming Browserbase sessions */,
modelName: "gpt-4o" /* Name of the model to use */,
modelClientOptions: {
apiKey: process.env.OPENAI_API_KEY,
apiKey: process.env.OPENAI_API_KEY || "dummy-api-key-for-browserbase",
} /* Configuration options for the model client */,
useAPI: false,
};
Expand All @@ -74,20 +75,48 @@ let stagehand: Stagehand | undefined;

// Ensure Stagehand is initialized
export async function ensureStagehand() {
// We've added a default CDP URL in the config, so this check should always pass
if (
stagehandConfig.env === "LOCAL" &&
!stagehandConfig.localBrowserLaunchOptions?.cdpUrl
) {
throw new Error(
'Using a local browser without providing a CDP URL is not supported. Please provide a CDP URL using the LOCAL_CDP_URL environment variable.\n\nTo launch your browser in "debug", see our documentation.\n\nhttps://docs.stagehand.dev/examples/customize_browser#use-your-personal-browser'
);
log("No CDP URL provided, using http://localhost:9222 as fallback", "info");
stagehandConfig.localBrowserLaunchOptions = {
cdpUrl: "http://localhost:9222"
};
}

try {
// If stagehand doesn't exist, create a new instance
if (!stagehand) {
log("Creating new Stagehand instance", "info");
stagehand = new Stagehand(stagehandConfig);
await stagehand.init();
return stagehand;
try {
await stagehand.init();
log("Successfully initialized new Stagehand instance", "info");
return stagehand;
} catch (initError) {
const initErrorMsg = initError instanceof Error ? initError.message : String(initError);
log(`Failed to initialize new Stagehand instance: ${initErrorMsg}`, "error");
throw initError;
}
}

// If stagehand exists but page is not initialized
if (!stagehand.page) {
log("Stagehand exists but page is not initialized, reinitializing...", "info");
try {
await stagehand.init();
log("Successfully reinitialized Stagehand page", "info");
return stagehand;
} catch (initError) {
const initErrorMsg = initError instanceof Error ? initError.message : String(initError);
log(`Failed to reinitialize Stagehand page: ${initErrorMsg}`, "error");
// Create a new instance as fallback
stagehand = new Stagehand(stagehandConfig);
await stagehand.init();
return stagehand;
}
}

// Try to perform a simple operation to check if the session is still valid
Expand Down
126 changes: 121 additions & 5 deletions stagehand/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export const TOOLS: Tool[] = [
"Takes a screenshot of the current page. Use this tool to learn where you are on the page when controlling the browser with Stagehand. Only use this tool when the other tools are not sufficient to get the information you need.",
inputSchema: {
type: "object",
properties: {},
properties: {
path: {
type: "string",
description: "Optional path where the screenshot should be saved (e.g., 'screenshot.png')"
}
},
},
},
];
Expand All @@ -86,9 +91,66 @@ export async function handleToolCall(
args: any,
stagehand: Stagehand
): Promise<CallToolResult> {
// Ensure stagehand is properly initialized
if (!stagehand) {
return {
content: [
{
type: "text",
text: "Stagehand is not initialized properly",
},
{
type: "text",
text: `Operation logs:\n${operationLogs.join("\n")}`,
},
],
isError: true,
};
}

// Make sure init has been called
if (!stagehand.page) {
try {
await stagehand.init();
console.log("Re-initialized Stagehand");
} catch (initError) {
const initErrorMsg = initError instanceof Error ? initError.message : String(initError);
return {
content: [
{
type: "text",
text: `Failed to initialize Stagehand: ${initErrorMsg}`,
},
{
type: "text",
text: `Operation logs:\n${operationLogs.join("\n")}`,
},
],
isError: true,
};
}
}

switch (name) {
case "stagehand_navigate":
try {
// Make sure we have a valid page
if (!stagehand || !stagehand.page) {
return {
content: [
{
type: "text",
text: "Unable to navigate: Stagehand instance or page not initialized",
},
{
type: "text",
text: `Operation logs:\n${operationLogs.join("\n")}`,
},
],
isError: true,
};
}

await stagehand.page.goto(args.url);
return {
content: [
Expand All @@ -98,7 +160,7 @@ export async function handleToolCall(
},
{
type: "text",
text: `View the live session here: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`,
text: `View the live session here: https://browserbase.com/sessions/${stagehand.browserbaseSessionID || 'not-available'}`,
},
],
isError: false,
Expand Down Expand Up @@ -235,9 +297,59 @@ export async function handleToolCall(

case "screenshot":
try {
const screenshotBuffer = await stagehand.page.screenshot({
// Make sure we have a valid page
if (!stagehand || !stagehand.page) {
return {
content: [
{
type: "text",
text: "Unable to take screenshot: Stagehand instance or page not initialized",
},
{
type: "text",
text: `Operation logs:\n${operationLogs.join("\n")}`,
},
],
isError: true,
};
}

// Check if the page is ready
try {
await stagehand.page.evaluate(() => document.title);
} catch (pageError) {
// If page is not ready, try to navigate to google.com as a baseline
try {
await stagehand.page.goto("https://www.google.com");
console.log("Navigated to Google as a fallback for screenshot");
} catch (navError) {
return {
content: [
{
type: "text",
text: `Failed to prepare page for screenshot: ${String(navError)}`,
},
{
type: "text",
text: `Operation logs:\n${operationLogs.join("\n")}`,
},
],
isError: true,
};
}
}

// Create screenshot options
const screenshotOptions: any = {
fullPage: false,
});
};

// Add path if provided
if (args.path) {
screenshotOptions.path = args.path;
}

const screenshotBuffer = await stagehand.page.screenshot(screenshotOptions);

// Convert buffer to base64 string and store in memory
const screenshotBase64 = screenshotBuffer.toString("base64");
Expand All @@ -254,11 +366,15 @@ export async function handleToolCall(
});
}

const saveMessage = args.path
? `Screenshot saved to path: ${args.path} and`
: "Screenshot";

return {
content: [
{
type: "text",
text: `Screenshot taken with name: ${name}`,
text: `${saveMessage} taken with name: ${name}`,
},
{
type: "image",
Expand Down