diff --git a/.changeset/four-hoops-mix.md b/.changeset/four-hoops-mix.md new file mode 100644 index 000000000..6848e8201 --- /dev/null +++ b/.changeset/four-hoops-mix.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": minor +--- + +add custom error classes diff --git a/evals/index.eval.ts b/evals/index.eval.ts index 0b265b4d6..008ebbcdc 100644 --- a/evals/index.eval.ts +++ b/evals/index.eval.ts @@ -31,6 +31,7 @@ import { EvalLogger } from "./logger"; import { AvailableModel } from "@/dist"; import { env } from "./env"; import dotenv from "dotenv"; +import { StagehandEvalError } from "@/types/stagehandErrors"; dotenv.config(); /** @@ -226,8 +227,8 @@ const generateFilteredTestcases = (): Testcase[] => { const taskFunction = taskModule[input.name]; if (typeof taskFunction !== "function") { - throw new Error( - `Task function for ${input.name} is not a function`, + throw new StagehandEvalError( + `No Eval function found for task name: ${input.name}`, ); } diff --git a/evals/taskConfig.ts b/evals/taskConfig.ts index 0031b8a80..590467b5c 100644 --- a/evals/taskConfig.ts +++ b/evals/taskConfig.ts @@ -14,6 +14,7 @@ import fs from "fs"; import path from "path"; import { AvailableModel, AvailableModelSchema } from "@/dist"; import { filterByEvalName } from "./args"; +import { UnsupportedModelError } from "@/types/stagehandErrors"; // The configuration file `evals.config.json` contains a list of tasks and their associated categories. const configPath = path.join(__dirname, "evals.config.json"); @@ -62,7 +63,7 @@ const getModelList = (): string[] => { }; const MODELS: AvailableModel[] = getModelList().map((model) => { if (!AvailableModelSchema.safeParse(model).success) { - throw new Error(`Model ${model} is not a supported model`); + throw new UnsupportedModelError(getModelList(), "Running evals"); } return model as AvailableModel; }); diff --git a/evals/tasks/peeler_simple.ts b/evals/tasks/peeler_simple.ts index 040025926..f8e6dbaeb 100644 --- a/evals/tasks/peeler_simple.ts +++ b/evals/tasks/peeler_simple.ts @@ -1,5 +1,6 @@ import { EvalFunction } from "@/types/evals"; import { initStagehand } from "@/evals/initStagehand"; +import { StagehandEnvironmentError } from "@/types/stagehandErrors"; const env: "BROWSERBASE" | "LOCAL" = process.env.EVAL_ENV?.toLowerCase() === "browserbase" @@ -15,8 +16,10 @@ export const peeler_simple: EvalFunction = async ({ modelName, logger }) => { const { debugUrl, sessionUrl } = initResponse; if (env === "BROWSERBASE") { - throw new Error( - "Browserbase not supported for this eval since we block all requests to file://", + throw new StagehandEnvironmentError( + "BROWSERBASE", + "LOCAL", + "peeler_simple eval", ); } diff --git a/examples/external_clients/ollama.ts b/examples/external_clients/ollama.ts index 8b633f493..a98af0501 100644 --- a/examples/external_clients/ollama.ts +++ b/examples/external_clients/ollama.ts @@ -26,6 +26,7 @@ import type { ChatCompletionUserMessageParam, } from "openai/resources/chat/completions"; import { z } from "zod"; +import { CreateChatCompletionResponseError } from "@/types/stagehandErrors"; function validateZodSchema(schema: z.ZodTypeAny, data: unknown) { try { @@ -226,7 +227,7 @@ export class OllamaClient extends LLMClient { if (options.response_model) { const extractedData = response.choices[0].message.content; if (!extractedData) { - throw new Error("No content in response"); + throw new CreateChatCompletionResponseError("No content in response"); } const parsedData = JSON.parse(extractedData); @@ -239,7 +240,7 @@ export class OllamaClient extends LLMClient { }); } - throw new Error("Invalid response schema"); + throw new CreateChatCompletionResponseError("Invalid response schema"); } return parsedData; diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 29adf7729..cdf08ad4e 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -19,6 +19,19 @@ import { LLMClient } from "./llm/LLMClient"; import { StagehandContext } from "./StagehandContext"; import { EnhancedContext } from "../types/context"; import { clearOverlays } from "./utils"; +import { + StagehandError, + StagehandNotInitializedError, + StagehandEnvironmentError, + CaptchaTimeoutError, + StagehandNotImplementedError, + StagehandDeprecationError, + BrowserbaseSessionNotFoundError, + MissingLLMConfigurationError, + HandlerNotInitializedError, + StagehandDefaultError, +} from "../types/stagehandErrors"; +import { StagehandAPIError } from "@/types/stagehandApiErrors"; const BROWSERBASE_REGION_DOMAIN = { "us-west-2": "wss://connect.usw2.browserbase.com", @@ -67,9 +80,7 @@ export class StagehandPage { prop === ("on" as keyof Page)) ) { return () => { - throw new Error( - `You seem to be calling \`${String(prop)}\` on a page in an uninitialized \`Stagehand\` object. Ensure you are running \`await stagehand.init()\` on the Stagehand object before referencing the \`page\` object.`, - ); + throw new StagehandNotInitializedError(String(prop)); }; } @@ -127,7 +138,7 @@ export class StagehandPage { const sessionId = this.stagehand.browserbaseSessionID; if (!sessionId) { - throw new Error("No Browserbase session ID found"); + throw new BrowserbaseSessionNotFoundError(); } const browserbase = new Browserbase({ @@ -171,14 +182,16 @@ export class StagehandPage { * Waits for a captcha to be solved when using Browserbase environment. * * @param timeoutMs - Optional timeout in milliseconds. If provided, the promise will reject if the captcha solving hasn't started within the given time. - * @throws Error if called in a LOCAL environment - * @throws Error if the timeout is reached before captcha solving starts + * @throws StagehandEnvironmentError if called in a LOCAL environment + * @throws CaptchaTimeoutError if the timeout is reached before captcha solving starts * @returns Promise that resolves when the captcha is solved */ public async waitForCaptchaSolve(timeoutMs?: number) { if (this.stagehand.env === "LOCAL") { - throw new Error( - "The waitForCaptcha method may only be used when using the Browserbase environment.", + throw new StagehandEnvironmentError( + this.stagehand.env, + "BROWSERBASE", + "waitForCaptcha method", ); } @@ -195,7 +208,7 @@ export class StagehandPage { if (timeoutMs) { timeoutId = setTimeout(() => { if (!started) { - reject(new Error("Captcha timeout")); + reject(new CaptchaTimeoutError()); } }, timeoutMs); } @@ -222,155 +235,160 @@ export class StagehandPage { } async init(): Promise { - const page = this.intPage; - const stagehand = this.stagehand; - - // Create a proxy that updates active page on method calls - const handler = { - get: (target: PlaywrightPage, prop: string | symbol) => { - const value = target[prop as keyof PlaywrightPage]; - - // Handle enhanced methods - if (prop === "act" || prop === "extract" || prop === "observe") { - if (!this.llmClient) { - return () => { - throw new Error( - "No LLM API key or LLM Client configured. An LLM API key or a custom LLM Client is required to use act, extract, or observe.", - ); - }; - } - - // Use type assertion to safely call the method with proper typing - type EnhancedMethod = ( - options: - | ActOptions - | ExtractOptions - | ObserveOptions, - ) => Promise< - ActResult | ExtractResult | ObserveResult[] - >; - - const method = this[prop as keyof StagehandPage] as EnhancedMethod; - return async (options: unknown) => { - this.intContext.setActivePage(this); - return method.call(this, options); - }; - } + try { + const page = this.intPage; + const stagehand = this.stagehand; + + // Create a proxy that updates active page on method calls + const handler = { + get: (target: PlaywrightPage, prop: string | symbol) => { + const value = target[prop as keyof PlaywrightPage]; + + // Handle enhanced methods + if (prop === "act" || prop === "extract" || prop === "observe") { + if (!this.llmClient) { + return () => { + throw new MissingLLMConfigurationError(); + }; + } - // Handle screenshots with CDP - if (prop === "screenshot") { - return async ( - options: { - type?: "png" | "jpeg"; - quality?: number; - fullPage?: boolean; - clip?: { x: number; y: number; width: number; height: number }; - omitBackground?: boolean; - } = {}, - ) => { - const cdpOptions: Record = { - format: options.type === "jpeg" ? "jpeg" : "png", - quality: options.quality, - clip: options.clip, - omitBackground: options.omitBackground, - fromSurface: true, + // Use type assertion to safely call the method with proper typing + type EnhancedMethod = ( + options: + | ActOptions + | ExtractOptions + | ObserveOptions, + ) => Promise< + ActResult | ExtractResult | ObserveResult[] + >; + + const method = this[prop as keyof StagehandPage] as EnhancedMethod; + return async (options: unknown) => { + this.intContext.setActivePage(this); + return method.call(this, options); }; + } - if (options.fullPage) { - cdpOptions.captureBeyondViewport = true; - } + // Handle screenshots with CDP + if (prop === "screenshot") { + return async ( + options: { + type?: "png" | "jpeg"; + quality?: number; + fullPage?: boolean; + clip?: { x: number; y: number; width: number; height: number }; + omitBackground?: boolean; + } = {}, + ) => { + const cdpOptions: Record = { + format: options.type === "jpeg" ? "jpeg" : "png", + quality: options.quality, + clip: options.clip, + omitBackground: options.omitBackground, + fromSurface: true, + }; + + if (options.fullPage) { + cdpOptions.captureBeyondViewport = true; + } - const data = await this.sendCDP<{ data: string }>( - "Page.captureScreenshot", - cdpOptions, - ); + const data = await this.sendCDP<{ data: string }>( + "Page.captureScreenshot", + cdpOptions, + ); - // Convert base64 to buffer - const buffer = Buffer.from(data.data, "base64"); + // Convert base64 to buffer + const buffer = Buffer.from(data.data, "base64"); - return buffer; - }; - } - - // Handle goto specially - if (prop === "goto") { - return async (url: string, options: GotoOptions) => { - this.intContext.setActivePage(this); - const result = this.api - ? await this.api.goto(url, options) - : await target.goto(url, options); + return buffer; + }; + } - this.addToHistory("navigate", { url, options }, result); + // Handle goto specially + if (prop === "goto") { + return async (url: string, options: GotoOptions) => { + this.intContext.setActivePage(this); + const result = this.api + ? await this.api.goto(url, options) + : await target.goto(url, options); + + this.addToHistory("navigate", { url, options }, result); + + if (this.waitForCaptchaSolves) { + try { + await this.waitForCaptchaSolve(1000); + } catch { + // ignore + } + } - if (this.waitForCaptchaSolves) { - try { - await this.waitForCaptchaSolve(1000); - } catch { - // ignore + if (this.api) { + await this._refreshPageFromAPI(); + } else { + if (stagehand.debugDom) { + this.stagehand.log({ + category: "deprecation", + message: + "Warning: debugDom is not supported in this version of Stagehand", + level: 1, + }); + } + await target.waitForLoadState("domcontentloaded"); + await this._waitForSettledDom(); } - } + return result; + }; + } - if (this.api) { - await this._refreshPageFromAPI(); - } else { - if (stagehand.debugDom) { - this.stagehand.log({ - category: "deprecation", - message: - "Warning: debugDom is not supported in this version of Stagehand", - level: 1, + // Handle event listeners + if (prop === "on") { + return ( + event: keyof PlaywrightPage["on"], + listener: Parameters[1], + ) => { + if (event === "popup") { + return this.context.on("page", async (page: PlaywrightPage) => { + const newContext = await StagehandContext.init( + page.context(), + stagehand, + ); + const newStagehandPage = new StagehandPage( + page, + stagehand, + newContext, + this.llmClient, + ); + + await newStagehandPage.init(); + listener(newStagehandPage.page); }); } - await target.waitForLoadState("domcontentloaded"); - await this._waitForSettledDom(); - } - return result; - }; - } - - // Handle event listeners - if (prop === "on") { - return ( - event: keyof PlaywrightPage["on"], - listener: Parameters[1], - ) => { - if (event === "popup") { - return this.context.on("page", async (page: PlaywrightPage) => { - const newContext = await StagehandContext.init( - page.context(), - stagehand, - ); - const newStagehandPage = new StagehandPage( - page, - stagehand, - newContext, - this.llmClient, - ); - - await newStagehandPage.init(); - listener(newStagehandPage.page); - }); - } - this.intContext.setActivePage(this); - return target.on(event, listener); - }; - } - - // For all other method calls, update active page - if (typeof value === "function") { - return (...args: unknown[]) => { - this.intContext.setActivePage(this); - return value.apply(target, args); - }; - } + this.intContext.setActivePage(this); + return target.on(event, listener); + }; + } - return value; - }, - }; + // For all other method calls, update active page + if (typeof value === "function") { + return (...args: unknown[]) => { + this.intContext.setActivePage(this); + return value.apply(target, args); + }; + } - this.intPage = new Proxy(page, handler) as unknown as Page; - this.initialized = true; - return this; + return value; + }, + }; + + this.intPage = new Proxy(page, handler) as unknown as Page; + this.initialized = true; + return this; + } catch (err: unknown) { + if (err instanceof StagehandError || err instanceof StagehandAPIError) { + throw err; + } + throw new StagehandDefaultError(); + } } public get page(): Page { @@ -464,391 +482,412 @@ export class StagehandPage { async act( actionOrOptions: string | ActOptions | ObserveResult, ): Promise { - if (!this.actHandler) { - throw new Error("Act handler not initialized"); - } - - await clearOverlays(this.page); + try { + if (!this.actHandler) { + throw new HandlerNotInitializedError("Act"); + } - // If actionOrOptions is an ObserveResult, we call actFromObserveResult. - // We need to ensure there is both a selector and a method in the ObserveResult. - if (typeof actionOrOptions === "object" && actionOrOptions !== null) { - // If it has selector AND method => treat as ObserveResult - if ("selector" in actionOrOptions && "method" in actionOrOptions) { - const observeResult = actionOrOptions as ObserveResult; - // validate observeResult.method, etc. - return this.actHandler.actFromObserveResult(observeResult); - } else { - // If it's an object but no selector/method, - // check that it's truly ActOptions (i.e., has an `action` field). - if (!("action" in actionOrOptions)) { - throw new Error( - "Invalid argument. Valid arguments are: a string, an ActOptions object, " + - "or an ObserveResult WITH 'selector' and 'method' fields.", - ); + await clearOverlays(this.page); + + // If actionOrOptions is an ObserveResult, we call actFromObserveResult. + // We need to ensure there is both a selector and a method in the ObserveResult. + if (typeof actionOrOptions === "object" && actionOrOptions !== null) { + // If it has selector AND method => treat as ObserveResult + if ("selector" in actionOrOptions && "method" in actionOrOptions) { + const observeResult = actionOrOptions as ObserveResult; + // validate observeResult.method, etc. + return this.actHandler.actFromObserveResult(observeResult); + } else { + // If it's an object but no selector/method, + // check that it's truly ActOptions (i.e., has an `action` field). + if (!("action" in actionOrOptions)) { + throw new StagehandError( + "Invalid argument. Valid arguments are: a string, an ActOptions object, " + + "or an ObserveResult WITH 'selector' and 'method' fields.", + ); + } } + } else if (typeof actionOrOptions === "string") { + // Convert string to ActOptions + actionOrOptions = { action: actionOrOptions }; + } else { + throw new StagehandError( + "Invalid argument: you may have called act with an empty ObserveResult.\n" + + "Valid arguments are: a string, an ActOptions object, or an ObserveResult " + + "WITH 'selector' and 'method' fields.", + ); } - } else if (typeof actionOrOptions === "string") { - // Convert string to ActOptions - actionOrOptions = { action: actionOrOptions }; - } else { - throw new Error( - "Invalid argument: you may have called act with an empty ObserveResult.\n" + - "Valid arguments are: a string, an ActOptions object, or an ObserveResult " + - "WITH 'selector' and 'method' fields.", - ); - } - const { - action, - modelName, - modelClientOptions, - useVision, // still destructure this but will not pass it on - variables = {}, - domSettleTimeoutMs, - slowDomBasedAct = true, - timeoutMs = this.stagehand.actTimeoutMs, - } = actionOrOptions; - - if (typeof useVision !== "undefined") { - this.stagehand.log({ - category: "deprecation", - message: - "Warning: vision is not supported in this version of Stagehand", - level: 1, - }); - } - - if (this.api) { - const result = await this.api.act(actionOrOptions); - await this._refreshPageFromAPI(); - this.addToHistory("act", actionOrOptions, result); - return result; - } - - const requestId = Math.random().toString(36).substring(2); - const llmClient: LLMClient = modelName - ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) - : this.llmClient; - - if (!slowDomBasedAct) { - return this.actHandler.observeAct( - actionOrOptions, - this.observeHandler, - llmClient, - requestId, - ); - } - - this.stagehand.log({ - category: "act", - message: "running act", - level: 1, - auxiliary: { - action: { - value: action, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - modelName: { - value: llmClient.modelName, - type: "string", - }, - }, - }); - - // `useVision` is no longer passed to the handler - const result = await this.actHandler - .act({ + const { action, - llmClient, - chunksSeen: [], - requestId, - variables, - previousSelectors: [], - skipActionCacheForThisStep: false, + modelName, + modelClientOptions, + useVision, // still destructure this but will not pass it on + variables = {}, domSettleTimeoutMs, - timeoutMs, - }) - .catch((e) => { + slowDomBasedAct = true, + timeoutMs = this.stagehand.actTimeoutMs, + } = actionOrOptions; + + if (typeof useVision !== "undefined") { this.stagehand.log({ - category: "act", - message: "error acting", + category: "deprecation", + message: + "Warning: vision is not supported in this version of Stagehand", level: 1, - auxiliary: { - error: { - value: e.message, - type: "string", - }, - trace: { - value: e.stack, - type: "string", - }, - }, }); + } + + if (this.api) { + const result = await this.api.act(actionOrOptions); + await this._refreshPageFromAPI(); + this.addToHistory("act", actionOrOptions, result); + return result; + } - return { - success: false, - message: `Internal error: Error acting: ${e.message}`, - action: action, - }; + const requestId = Math.random().toString(36).substring(2); + const llmClient: LLMClient = modelName + ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) + : this.llmClient; + + if (!slowDomBasedAct) { + return this.actHandler.observeAct( + actionOrOptions, + this.observeHandler, + llmClient, + requestId, + ); + } + + this.stagehand.log({ + category: "act", + message: "running act", + level: 1, + auxiliary: { + action: { + value: action, + type: "string", + }, + requestId: { + value: requestId, + type: "string", + }, + modelName: { + value: llmClient.modelName, + type: "string", + }, + }, }); - this.addToHistory("act", actionOrOptions, result); + // `useVision` is no longer passed to the handler + const result = await this.actHandler + .act({ + action, + llmClient, + chunksSeen: [], + requestId, + variables, + previousSelectors: [], + skipActionCacheForThisStep: false, + domSettleTimeoutMs, + timeoutMs, + }) + .catch((e) => { + this.stagehand.log({ + category: "act", + message: "error acting", + level: 1, + auxiliary: { + error: { + value: e.message, + type: "string", + }, + trace: { + value: e.stack, + type: "string", + }, + }, + }); + + return { + success: false, + message: `Internal error: Error acting: ${e.message}`, + action: action, + }; + }); + + this.addToHistory("act", actionOrOptions, result); - return result; + return result; + } catch (err: unknown) { + if (err instanceof StagehandError || err instanceof StagehandAPIError) { + throw err; + } + throw new StagehandDefaultError(); + } } async extract( instructionOrOptions?: string | ExtractOptions, ): Promise> { - if (!this.extractHandler) { - throw new Error("Extract handler not initialized"); - } - - await clearOverlays(this.page); - - // check if user called extract() with no arguments - if (!instructionOrOptions) { - let result: ExtractResult; - if (this.api) { - result = await this.api.extract({}); - } else { - result = await this.extractHandler.extract(); + try { + if (!this.extractHandler) { + throw new HandlerNotInitializedError("Extract"); } - this.addToHistory("extract", instructionOrOptions, result); - return result; - } - const options: ExtractOptions = - typeof instructionOrOptions === "string" - ? { - instruction: instructionOrOptions, - schema: defaultExtractSchema as T, - } - : instructionOrOptions; - - const { - instruction, - schema, - modelName, - modelClientOptions, - domSettleTimeoutMs, - useTextExtract, - selector, - } = options; - - // Throw a NotImplementedError if the user passed in an `xpath` - // and `useTextExtract` is false - if (selector && useTextExtract !== true) { - throw new Error( - "NotImplementedError: Passing an xpath into extract is only supported when `useTextExtract: true`.", - ); - } + await clearOverlays(this.page); - if (this.api) { - const result = await this.api.extract(options); - this.addToHistory("extract", instructionOrOptions, result); - return result; - } - - const requestId = Math.random().toString(36).substring(2); - const llmClient = modelName - ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) - : this.llmClient; + // check if user called extract() with no arguments + if (!instructionOrOptions) { + let result: ExtractResult; + if (this.api) { + result = await this.api.extract({}); + } else { + result = await this.extractHandler.extract(); + } + this.addToHistory("extract", instructionOrOptions, result); + return result; + } - this.stagehand.log({ - category: "extract", - message: "running extract", - level: 1, - auxiliary: { - instruction: { - value: instruction, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - modelName: { - value: llmClient.modelName, - type: "string", - }, - }, - }); + const options: ExtractOptions = + typeof instructionOrOptions === "string" + ? { + instruction: instructionOrOptions, + schema: defaultExtractSchema as T, + } + : instructionOrOptions; - const result = await this.extractHandler - .extract({ + const { instruction, schema, - llmClient, - requestId, + modelName, + modelClientOptions, domSettleTimeoutMs, useTextExtract, selector, - }) - .catch((e) => { - this.stagehand.log({ - category: "extract", - message: "error extracting", - level: 1, - auxiliary: { - error: { - value: e.message, - type: "string", - }, - trace: { - value: e.stack, - type: "string", - }, - }, - }); - - if (this.stagehand.enableCaching) { - this.stagehand.llmProvider.cleanRequestCache(requestId); - } - - throw e; - }); + } = options; + + // Throw a NotImplementedError if the user passed in an `xpath` + // and `useTextExtract` is false + if (selector && useTextExtract !== true) { + throw new StagehandNotImplementedError( + "Passing an xpath into extract is only supported when `useTextExtract: true`.", + ); + } - this.addToHistory("extract", instructionOrOptions, result); + if (this.api) { + const result = await this.api.extract(options); + this.addToHistory("extract", instructionOrOptions, result); + return result; + } - return result; - } + const requestId = Math.random().toString(36).substring(2); + const llmClient = modelName + ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) + : this.llmClient; - async observe( - instructionOrOptions?: string | ObserveOptions, - ): Promise { - if (!this.observeHandler) { - throw new Error("Observe handler not initialized"); - } - - await clearOverlays(this.page); - - const options: ObserveOptions = - typeof instructionOrOptions === "string" - ? { instruction: instructionOrOptions } - : instructionOrOptions || {}; - - const { - instruction, - modelName, - modelClientOptions, - useVision, // still destructure but will not pass it on - domSettleTimeoutMs, - returnAction = true, - onlyVisible = false, - useAccessibilityTree, - drawOverlay, - } = options; - - if (useAccessibilityTree !== undefined) { this.stagehand.log({ - category: "deprecation", - message: - "useAccessibilityTree is deprecated.\n" + - " To use accessibility tree as context:\n" + - " 1. Set onlyVisible to false (default)\n" + - " 2. Don't declare useAccessibilityTree", + category: "extract", + message: "running extract", level: 1, + auxiliary: { + instruction: { + value: instruction, + type: "string", + }, + requestId: { + value: requestId, + type: "string", + }, + modelName: { + value: llmClient.modelName, + type: "string", + }, + }, }); - throw new Error( - "useAccessibilityTree is deprecated. Use onlyVisible instead.", - ); - } - if (typeof useVision !== "undefined") { - this.stagehand.log({ - category: "deprecation", - message: - "Warning: vision is not supported in this version of Stagehand", - level: 1, - }); - } + const result = await this.extractHandler + .extract({ + instruction, + schema, + llmClient, + requestId, + domSettleTimeoutMs, + useTextExtract, + selector, + }) + .catch((e) => { + this.stagehand.log({ + category: "extract", + message: "error extracting", + level: 1, + auxiliary: { + error: { + value: e.message, + type: "string", + }, + trace: { + value: e.stack, + type: "string", + }, + }, + }); + + if (this.stagehand.enableCaching) { + this.stagehand.llmProvider.cleanRequestCache(requestId); + } + + throw e; + }); + + this.addToHistory("extract", instructionOrOptions, result); - if (this.api) { - const result = await this.api.observe(options); - this.addToHistory("observe", instructionOrOptions, result); return result; + } catch (err: unknown) { + if (err instanceof StagehandError || err instanceof StagehandAPIError) { + throw err; + } + throw new StagehandDefaultError(); } + } - const requestId = Math.random().toString(36).substring(2); - const llmClient = modelName - ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) - : this.llmClient; + async observe( + instructionOrOptions?: string | ObserveOptions, + ): Promise { + try { + if (!this.observeHandler) { + throw new HandlerNotInitializedError("Observe"); + } - this.stagehand.log({ - category: "observe", - message: "running observe", - level: 1, - auxiliary: { - instruction: { - value: instruction, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - modelName: { - value: llmClient.modelName, - type: "string", - }, - onlyVisible: { - value: onlyVisible ? "true" : "false", - type: "boolean", - }, - }, - }); + await clearOverlays(this.page); - const result = await this.observeHandler - .observe({ + const options: ObserveOptions = + typeof instructionOrOptions === "string" + ? { instruction: instructionOrOptions } + : instructionOrOptions || {}; + + const { instruction, - llmClient, - requestId, + modelName, + modelClientOptions, + useVision, // still destructure but will not pass it on domSettleTimeoutMs, - returnAction, - onlyVisible, + returnAction = true, + onlyVisible = false, + useAccessibilityTree, drawOverlay, - }) - .catch((e) => { + } = options; + + if (useAccessibilityTree !== undefined) { this.stagehand.log({ - category: "observe", - message: "error observing", + category: "deprecation", + message: + "useAccessibilityTree is deprecated.\n" + + " To use accessibility tree as context:\n" + + " 1. Set onlyVisible to false (default)\n" + + " 2. Don't declare useAccessibilityTree", level: 1, - auxiliary: { - error: { - value: e.message, - type: "string", - }, - trace: { - value: e.stack, - type: "string", - }, - requestId: { - value: requestId, - type: "string", - }, - instruction: { - value: instruction, - type: "string", - }, - }, }); + throw new StagehandDeprecationError( + "useAccessibilityTree is deprecated. Use onlyVisible instead.", + ); + } - if (this.stagehand.enableCaching) { - this.stagehand.llmProvider.cleanRequestCache(requestId); - } + if (typeof useVision !== "undefined") { + this.stagehand.log({ + category: "deprecation", + message: + "Warning: vision is not supported in this version of Stagehand", + level: 1, + }); + } + + if (this.api) { + const result = await this.api.observe(options); + this.addToHistory("observe", instructionOrOptions, result); + return result; + } - throw e; + const requestId = Math.random().toString(36).substring(2); + const llmClient = modelName + ? this.stagehand.llmProvider.getClient(modelName, modelClientOptions) + : this.llmClient; + + this.stagehand.log({ + category: "observe", + message: "running observe", + level: 1, + auxiliary: { + instruction: { + value: instruction, + type: "string", + }, + requestId: { + value: requestId, + type: "string", + }, + modelName: { + value: llmClient.modelName, + type: "string", + }, + onlyVisible: { + value: onlyVisible ? "true" : "false", + type: "boolean", + }, + }, }); - this.addToHistory("observe", instructionOrOptions, result); + const result = await this.observeHandler + .observe({ + instruction, + llmClient, + requestId, + domSettleTimeoutMs, + returnAction, + onlyVisible, + drawOverlay, + }) + .catch((e) => { + this.stagehand.log({ + category: "observe", + message: "error observing", + level: 1, + auxiliary: { + error: { + value: e.message, + type: "string", + }, + trace: { + value: e.stack, + type: "string", + }, + requestId: { + value: requestId, + type: "string", + }, + instruction: { + value: instruction, + type: "string", + }, + }, + }); + + if (this.stagehand.enableCaching) { + this.stagehand.llmProvider.cleanRequestCache(requestId); + } + + throw e; + }); + + this.addToHistory("observe", instructionOrOptions, result); - return result; + return result; + } catch (err: unknown) { + if (err instanceof StagehandError || err instanceof StagehandAPIError) { + throw err; + } + throw new StagehandDefaultError(); + } } async getCDPClient(): Promise { diff --git a/lib/agent/AgentProvider.ts b/lib/agent/AgentProvider.ts index c14f5bd81..1321fd3b9 100644 --- a/lib/agent/AgentProvider.ts +++ b/lib/agent/AgentProvider.ts @@ -3,6 +3,10 @@ import { AgentClient } from "./AgentClient"; import { AgentType } from "@/types/agent"; import { OpenAICUAClient } from "./OpenAICUAClient"; import { AnthropicCUAClient } from "./AnthropicCUAClient"; +import { + UnsupportedModelError, + UnsupportedModelProviderError, +} from "@/types/stagehandErrors"; // Map model names to their provider types const modelToAgentProviderMap: Record = { @@ -56,7 +60,10 @@ export class AgentProvider { clientOptions, ); default: - throw new Error(`Unknown agent type: ${type}`); + throw new UnsupportedModelProviderError( + ["openai", "anthropic"], + "Computer Use Agent", + ); } } catch (error) { const errorMessage = @@ -76,7 +83,9 @@ export class AgentProvider { return modelToAgentProviderMap[modelName]; } - // Default to OpenAI CUA for unrecognized models with warning - throw new Error(`Unknown model name: ${modelName}`); + throw new UnsupportedModelError( + Object.keys(modelToAgentProviderMap), + "Computer Use Agent", + ); } } diff --git a/lib/agent/AnthropicCUAClient.ts b/lib/agent/AnthropicCUAClient.ts index 69dd61abc..fbbc97d82 100644 --- a/lib/agent/AnthropicCUAClient.ts +++ b/lib/agent/AnthropicCUAClient.ts @@ -12,6 +12,7 @@ import { AnthropicToolResult, } from "@/types/agent"; import { AgentClient } from "./AgentClient"; +import { AgentScreenshotProviderError } from "@/types/stagehandErrors"; export type ResponseInputItem = AnthropicMessage | AnthropicToolResult; @@ -853,6 +854,9 @@ export class AnthropicCUAClient extends AgentClient { } } - throw new Error("Screenshot provider not available"); + throw new AgentScreenshotProviderError( + "`screenshotProvider` has not been set. " + + "Please call `setScreenshotProvider()` with a valid function that returns a base64-encoded image", + ); } } diff --git a/lib/agent/OpenAICUAClient.ts b/lib/agent/OpenAICUAClient.ts index 1bedb30ab..6a494300b 100644 --- a/lib/agent/OpenAICUAClient.ts +++ b/lib/agent/OpenAICUAClient.ts @@ -11,6 +11,7 @@ import { FunctionCallItem, } from "@/types/agent"; import { AgentClient } from "./AgentClient"; +import { AgentScreenshotProviderError } from "@/types/stagehandErrors"; /** * Client for OpenAI's Computer Use Assistant API @@ -573,6 +574,9 @@ export class OpenAICUAClient extends AgentClient { } } - throw new Error("Screenshot provider not available"); + throw new AgentScreenshotProviderError( + "`screenshotProvider` has not been set. " + + "Please call `setScreenshotProvider()` with a valid function that returns a base64-encoded image", + ); } } diff --git a/lib/api.ts b/lib/api.ts index b8d25fc11..76d3125ee 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -19,6 +19,14 @@ import { ObserveResult, } from "../types/stagehand"; import { AgentExecuteOptions, AgentResult } from "."; +import { + StagehandAPIUnauthorizedError, + StagehandHttpError, + StagehandAPIError, + StagehandServerError, + StagehandResponseBodyError, + StagehandResponseParseError, +} from "../types/stagehandApiErrors"; export class StagehandAPI { private apiKey: string; @@ -63,19 +71,19 @@ export class StagehandAPI { }); if (sessionResponse.status === 401) { - throw new Error( + throw new StagehandAPIUnauthorizedError( "Unauthorized. Ensure you provided a valid API key and that it is whitelisted.", ); } else if (sessionResponse.status !== 200) { console.log(await sessionResponse.text()); - throw new Error(`Unknown error: ${sessionResponse.status}`); + throw new StagehandHttpError(`Unknown error: ${sessionResponse.status}`); } const sessionResponseBody = (await sessionResponse.json()) as ApiResponse; if (sessionResponseBody.success === false) { - throw new Error(sessionResponseBody.message); + throw new StagehandAPIError(sessionResponseBody.message); } this.sessionId = sessionResponseBody.data.sessionId; @@ -153,13 +161,13 @@ export class StagehandAPI { if (!response.ok) { const errorBody = await response.text(); - throw new Error( + throw new StagehandHttpError( `HTTP error! status: ${response.status}, body: ${errorBody}`, ); } if (!response.body) { - throw new Error("Response body is null"); + throw new StagehandResponseBodyError(); } const reader = response.body.getReader(); @@ -185,7 +193,7 @@ export class StagehandAPI { if (eventData.type === "system") { if (eventData.data.status === "error") { - throw new Error(eventData.data.error); + throw new StagehandServerError(eventData.data.error); } if (eventData.data.status === "finished") { return eventData.data.result as T; @@ -195,7 +203,9 @@ export class StagehandAPI { } } catch (e) { console.error("Error parsing event data:", e); - throw new Error("Failed to parse server response"); + throw new StagehandResponseParseError( + "Failed to parse server response", + ); } } diff --git a/lib/dom/process.ts b/lib/dom/process.ts index c8291fbf4..221e441a9 100644 --- a/lib/dom/process.ts +++ b/lib/dom/process.ts @@ -10,6 +10,7 @@ import { StagehandContainer } from "./StagehandContainer"; import { GlobalPageContainer } from "@/lib/dom/GlobalPageContainer"; import { ElementContainer } from "@/lib/dom/ElementContainer"; import { DomChunk } from "@/lib/dom/DomChunk"; +import { StagehandDomProcessError } from "@/types/stagehandErrors"; /** * Finds and returns a list of scrollable elements on the page, @@ -553,7 +554,9 @@ async function pickChunk(chunksSeen: Array) { const chunk = closestChunk; if (chunk === undefined) { - throw new Error(`No chunks remaining to check: ${chunksRemaining}`); + throw new StagehandDomProcessError( + `No chunks remaining to check: ${chunksRemaining}`, + ); } return { chunk, diff --git a/lib/dom/utils.ts b/lib/dom/utils.ts index e987aee71..f60448fb9 100644 --- a/lib/dom/utils.ts +++ b/lib/dom/utils.ts @@ -1,3 +1,5 @@ +import { StagehandDomProcessError } from "@/types/stagehandErrors"; + export async function waitForDomSettle() { return new Promise((resolve) => { const createTimeout = () => { @@ -41,7 +43,7 @@ export function canElementScroll(elem: HTMLElement): boolean { // If scrollTop never changed, consider it unscrollable if (elem.scrollTop === originalTop) { - throw new Error("scrollTop did not change"); + throw new StagehandDomProcessError("scrollTop did not change"); } // Scroll back to original place diff --git a/lib/handlers/actHandler.ts b/lib/handlers/actHandler.ts index 83174403a..d4a87cbfd 100644 --- a/lib/handlers/actHandler.ts +++ b/lib/handlers/actHandler.ts @@ -26,6 +26,10 @@ import { } from "./handlerUtils/actHandlerUtils"; import { Stagehand } from "@/lib"; import { StagehandObserveHandler } from "@/lib/handlers/observeHandler"; +import { + StagehandElementNotFoundError, + StagehandInvalidArgumentError, +} from "@/types/stagehandErrors"; /** * NOTE: Vision support has been removed from this version of Stagehand. * If useVision or verifierUseVision is set to true, a warning is logged and @@ -224,7 +228,7 @@ export class StagehandActHandler { if (typeof actionOrOptions === "object" && actionOrOptions !== null) { if (!("action" in actionOrOptions)) { - throw new Error( + throw new StagehandInvalidArgumentError( "Invalid argument. Action options must have an `action` field.", ); } @@ -233,7 +237,9 @@ export class StagehandActHandler { typeof actionOrOptions.action !== "string" || actionOrOptions.action.length === 0 ) { - throw new Error("Invalid argument. No action provided."); + throw new StagehandInvalidArgumentError( + "Invalid argument. No action provided.", + ); } action = actionOrOptions.action; @@ -244,7 +250,7 @@ export class StagehandActHandler { if (actionOrOptions.modelClientOptions) observeOptions.modelClientOptions = actionOrOptions.modelClientOptions; } else { - throw new Error( + throw new StagehandInvalidArgumentError( "Invalid argument. Valid arguments are: a string, an ActOptions object with an `action` field not empty, or an ObserveResult with a `selector` and `method` field.", ); } @@ -745,7 +751,7 @@ export class StagehandActHandler { // If no XPath was valid, we cannot proceed if (!foundXpath || !locator) { - throw new Error("None of the provided XPaths could be located."); + throw new StagehandElementNotFoundError(xpaths); } const originalUrl = this.stagehandPage.page.url(); diff --git a/lib/handlers/operatorHandler.ts b/lib/handlers/operatorHandler.ts index 23ec2a7ce..ec5a7d5e5 100644 --- a/lib/handlers/operatorHandler.ts +++ b/lib/handlers/operatorHandler.ts @@ -11,6 +11,10 @@ import { ChatMessage, LLMClient } from "../llm/LLMClient"; import { buildOperatorSystemPrompt } from "../prompt"; import { StagehandPage } from "../StagehandPage"; import { ObserveResult } from "@/types/stagehand"; +import { + StagehandError, + StagehandMissingArgumentError, +} from "@/types/stagehandErrors"; export class StagehandOperatorHandler { private stagehandPage: StagehandPage; @@ -203,13 +207,18 @@ export class StagehandOperatorHandler { switch (method) { case "act": if (!playwrightArguments) { - throw new Error("No playwright arguments provided"); + throw new StagehandMissingArgumentError( + "No arguments provided to `act()`. " + + "Please ensure that all required arguments are passed in.", + ); } await page.act(playwrightArguments); break; case "extract": if (!extractionResult) { - throw new Error("No extraction result provided"); + throw new StagehandError( + "Error in OperatorHandler: Cannot complete extraction. No extractionResult provided.", + ); } return extractionResult; case "goto": @@ -225,7 +234,9 @@ export class StagehandOperatorHandler { await page.reload(); break; default: - throw new Error(`Unknown action: ${method}`); + throw new StagehandError( + `Error in OperatorHandler: Cannot execute unknown action: ${method}`, + ); } } } diff --git a/lib/index.ts b/lib/index.ts index b2ec2de49..22f1a8539 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -42,6 +42,14 @@ import { AgentExecuteOptions, AgentResult } from "../types/agent"; import { StagehandAgentHandler } from "./handlers/agentHandler"; import { StagehandOperatorHandler } from "./handlers/operatorHandler"; +import { + StagehandError, + StagehandNotInitializedError, + StagehandEnvironmentError, + MissingEnvironmentVariableError, + UnsupportedModelError, +} from "../types/stagehandErrors"; + dotenv.config({ path: ".env" }); const DEFAULT_MODEL_NAME = "gpt-4o"; @@ -84,7 +92,7 @@ async function getBrowser( if (env === "BROWSERBASE") { if (!apiKey) { - throw new Error("BROWSERBASE_API_KEY is required."); + throw new StagehandError("BROWSERBASE_API_KEY is required."); } let debugUrl: string | undefined = undefined; @@ -103,7 +111,7 @@ async function getBrowser( await browserbase.sessions.retrieve(browserbaseSessionID); if (sessionStatus.status !== "RUNNING") { - throw new Error( + throw new StagehandError( `Session ${browserbaseSessionID} is not running (status: ${sessionStatus.status})`, ); } @@ -152,7 +160,7 @@ async function getBrowser( }); if (!projectId) { - throw new Error( + throw new StagehandError( "BROWSERBASE_PROJECT_ID is required for new Browserbase sessions.", ); } @@ -393,9 +401,7 @@ export class Stagehand { public get page(): Page { if (!this.stagehandContext) { - throw new Error( - "Stagehand not initialized. Make sure to await stagehand.init() first.", - ); + throw new StagehandNotInitializedError("page"); } return this.stagehandPage.page; } @@ -520,19 +526,19 @@ export class Stagehand { this.actTimeoutMs = actTimeoutMs; if (this.usingAPI && env === "LOCAL") { - throw new Error("API mode can only be used with BROWSERBASE environment"); + throw new StagehandEnvironmentError("LOCAL", "BROWSERBASE", "API mode"); } else if (this.usingAPI && !process.env.STAGEHAND_API_URL) { - throw new Error( - "STAGEHAND_API_URL is required when using the API. Please set it in your environment variables.", + throw new MissingEnvironmentVariableError( + "STAGEHAND_API_URL", + "API mode", ); } else if ( this.usingAPI && + this.llmClient && this.llmClient.type !== "openai" && this.llmClient.type !== "anthropic" ) { - throw new Error( - "API mode requires an OpenAI or Anthropic LLM. Please provide a compatible model.", - ); + throw new UnsupportedModelError(["openai", "anthropic"], "API mode"); } this.waitForCaptchaSolves = waitForCaptchaSolves; @@ -580,9 +586,7 @@ export class Stagehand { public get context(): EnhancedContext { if (!this.stagehandContext) { - throw new Error( - "Stagehand not initialized. Make sure to await stagehand.init() first.", - ); + throw new StagehandNotInitializedError("context"); } return this.stagehandContext.context; } @@ -592,7 +596,7 @@ export class Stagehand { initOptions?: InitOptions, ): Promise { if (isRunningInBun()) { - throw new Error( + throw new StagehandError( "Playwright does not currently support the Bun runtime environment. " + "Please use Node.js instead. For more information, see: " + "https://github.com/microsoft/playwright/issues/27139", @@ -799,7 +803,7 @@ export class Stagehand { level: 0, }); } else { - throw new Error((body as ErrorResponse).message); + throw new StagehandError((body as ErrorResponse).message); } } return; @@ -867,14 +871,14 @@ export class Stagehand { : instructionOrOptions; if (!executeOptions.instruction) { - throw new Error("Instruction is required for agent execution"); + throw new StagehandError( + "Instruction is required for agent execution", + ); } if (this.usingAPI) { if (!this.apiClient) { - throw new Error( - "API client not initialized. Ensure that you have initialized Stagehand via `await stagehand.init()`.", - ); + throw new StagehandNotInitializedError("API client"); } if (!options.options) { @@ -888,7 +892,7 @@ export class Stagehand { } if (!options.options.apiKey) { - throw new Error( + throw new StagehandError( `API key not found for \`${options.provider}\` provider. Please set the ${options.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} environment variable or pass an apiKey in the options object.`, ); } @@ -903,9 +907,7 @@ export class Stagehand { public get history(): ReadonlyArray { if (!this.stagehandPage) { - throw new Error( - "History is only available after a page has been initialized", - ); + throw new StagehandNotInitializedError("history()"); } return this.stagehandPage.history; diff --git a/lib/llm/AnthropicClient.ts b/lib/llm/AnthropicClient.ts index a7d97316d..a2fe0b757 100644 --- a/lib/llm/AnthropicClient.ts +++ b/lib/llm/AnthropicClient.ts @@ -14,6 +14,7 @@ import { LLMClient, LLMResponse, } from "./LLMClient"; +import { CreateChatCompletionResponseError } from "@/types/stagehandErrors"; export class AnthropicClient extends LLMClient { public type = "anthropic" as const; @@ -339,8 +340,8 @@ export class AnthropicClient extends LLMClient { }, }, }); - throw new Error( - "Create Chat Completion Failed: No tool use with input in response", + throw new CreateChatCompletionResponseError( + "No tool use with input in response", ); } } diff --git a/lib/llm/CerebrasClient.ts b/lib/llm/CerebrasClient.ts index 22c4632aa..6685fa779 100644 --- a/lib/llm/CerebrasClient.ts +++ b/lib/llm/CerebrasClient.ts @@ -10,6 +10,7 @@ import { LLMClient, LLMResponse, } from "./LLMClient"; +import { CreateChatCompletionResponseError } from "@/types/stagehandErrors"; export class CerebrasClient extends LLMClient { public type = "cerebras" as const; @@ -296,9 +297,7 @@ export class CerebrasClient extends LLMClient { }); } - throw new Error( - "Create Chat Completion Failed: Could not extract valid JSON from response", - ); + throw new CreateChatCompletionResponseError("Invalid response schema"); } if (this.enableCaching) { diff --git a/lib/llm/GroqClient.ts b/lib/llm/GroqClient.ts index 206dbaa07..df317fb5f 100644 --- a/lib/llm/GroqClient.ts +++ b/lib/llm/GroqClient.ts @@ -10,6 +10,7 @@ import { LLMClient, LLMResponse, } from "./LLMClient"; +import { CreateChatCompletionResponseError } from "@/types/stagehandErrors"; export class GroqClient extends LLMClient { public type = "groq" as const; @@ -296,9 +297,7 @@ export class GroqClient extends LLMClient { }); } - throw new Error( - "Create Chat Completion Failed: Could not extract valid JSON from response", - ); + throw new CreateChatCompletionResponseError("Invalid response schema"); } if (this.enableCaching) { diff --git a/lib/llm/LLMProvider.ts b/lib/llm/LLMProvider.ts index 3ee3f29d8..1d9ff7e0a 100644 --- a/lib/llm/LLMProvider.ts +++ b/lib/llm/LLMProvider.ts @@ -10,6 +10,10 @@ import { CerebrasClient } from "./CerebrasClient"; import { GroqClient } from "./GroqClient"; import { LLMClient } from "./LLMClient"; import { OpenAIClient } from "./OpenAIClient"; +import { + UnsupportedModelError, + UnsupportedModelProviderError, +} from "@/types/stagehandErrors"; const modelToProviderMap: { [key in AvailableModel]: ModelProvider } = { "gpt-4o": "openai", @@ -66,7 +70,7 @@ export class LLMProvider { ): LLMClient { const provider = modelToProviderMap[modelName]; if (!provider) { - throw new Error(`Unsupported model: ${modelName}`); + throw new UnsupportedModelError(Object.keys(modelToProviderMap)); } switch (provider) { @@ -103,7 +107,9 @@ export class LLMProvider { clientOptions, }); default: - throw new Error(`Unsupported provider: ${provider}`); + throw new UnsupportedModelProviderError([ + ...new Set(Object.values(modelToProviderMap)), + ]); } } diff --git a/lib/llm/OpenAIClient.ts b/lib/llm/OpenAIClient.ts index ff8e8eff8..a8ee7464c 100644 --- a/lib/llm/OpenAIClient.ts +++ b/lib/llm/OpenAIClient.ts @@ -21,6 +21,10 @@ import { LLMClient, LLMResponse, } from "./LLMClient"; +import { + CreateChatCompletionResponseError, + StagehandError, +} from "@/types/stagehandErrors"; export class OpenAIClient extends LLMClient { public type = "openai" as const; @@ -84,7 +88,7 @@ export class OpenAIClient extends LLMClient { role: "user", })); if (options.tools && options.response_model) { - throw new Error( + throw new StagehandError( "Cannot use both tool and response_model for o1 models", ); } @@ -113,7 +117,7 @@ export class OpenAIClient extends LLMClient { options.temperature && (this.modelName.startsWith("o1") || this.modelName.startsWith("o3")) ) { - throw new Error("Temperature is not supported for o1 models"); + throw new StagehandError("Temperature is not supported for o1 models"); } const { image, requestId, ...optionsWithoutImageAndRequestId } = options; @@ -417,7 +421,7 @@ export class OpenAIClient extends LLMClient { }); } - throw new Error("Invalid response schema"); + throw new CreateChatCompletionResponseError("Invalid response schema"); } if (this.enableCaching) { diff --git a/types/stagehandApiErrors.ts b/types/stagehandApiErrors.ts new file mode 100644 index 000000000..a5f7da63d --- /dev/null +++ b/types/stagehandApiErrors.ts @@ -0,0 +1,36 @@ +export class StagehandAPIError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class StagehandAPIUnauthorizedError extends StagehandAPIError { + constructor(message?: string) { + super(message || "Unauthorized request"); + } +} + +export class StagehandHttpError extends StagehandAPIError { + constructor(message: string) { + super(message); + } +} + +export class StagehandServerError extends StagehandAPIError { + constructor(message: string) { + super(message); + } +} + +export class StagehandResponseBodyError extends StagehandAPIError { + constructor() { + super("Response body is null"); + } +} + +export class StagehandResponseParseError extends StagehandAPIError { + constructor(message: string) { + super(message); + } +} diff --git a/types/stagehandErrors.ts b/types/stagehandErrors.ts new file mode 100644 index 000000000..95fd3ccea --- /dev/null +++ b/types/stagehandErrors.ts @@ -0,0 +1,147 @@ +export class StagehandError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class StagehandDefaultError extends StagehandError { + constructor() { + super( + `\nHey! We're sorry you ran into an error. \nIf you need help, please open a Github issue or send us a Slack message: https://stagehand-dev.slack.com\n`, + ); + } +} + +export class StagehandEnvironmentError extends StagehandError { + constructor( + currentEnvironment: string, + requiredEnvironment: string, + feature: string, + ) { + super( + `You seem to be setting the current environment to ${currentEnvironment}.` + + `Ensure the environment is set to ${requiredEnvironment} if you want to use ${feature}.`, + ); + } +} + +export class MissingEnvironmentVariableError extends StagehandError { + constructor(missingEnvironmentVariable: string, feature: string) { + super( + `${missingEnvironmentVariable} is required to use ${feature}.` + + `Please set ${missingEnvironmentVariable} in your environment.`, + ); + } +} + +export class UnsupportedModelError extends StagehandError { + constructor(supportedModels: string[], feature?: string) { + super( + feature + ? `${feature} requires one of the following models: ${supportedModels}` + : `please use one of the supported models: ${supportedModels}`, + ); + } +} + +export class UnsupportedModelProviderError extends StagehandError { + constructor(supportedProviders: string[], feature?: string) { + super( + feature + ? `${feature} requires one of the following model providers: ${supportedProviders}` + : `please use one of the supported model providers: ${supportedProviders}`, + ); + } +} + +export class StagehandNotInitializedError extends StagehandError { + constructor(prop: string) { + super( + `You seem to be calling \`${prop}\` on a page in an uninitialized \`Stagehand\` object. ` + + `Ensure you are running \`await stagehand.init()\` on the Stagehand object before ` + + `referencing the \`page\` object.`, + ); + } +} + +export class BrowserbaseSessionNotFoundError extends StagehandError { + constructor() { + super("No Browserbase session ID found"); + } +} + +export class CaptchaTimeoutError extends StagehandError { + constructor() { + super("Captcha timeout"); + } +} + +export class MissingLLMConfigurationError extends StagehandError { + constructor() { + super( + "No LLM API key or LLM Client configured. An LLM API key or a custom LLM Client " + + "is required to use act, extract, or observe.", + ); + } +} + +export class HandlerNotInitializedError extends StagehandError { + constructor(handlerType: string) { + super(`${handlerType} handler not initialized`); + } +} + +export class StagehandNotImplementedError extends StagehandError { + constructor(message: string) { + super(`NotImplementedError: ${message}`); + } +} + +export class StagehandDeprecationError extends StagehandError { + constructor(message: string) { + super(`DeprecationError: ${message}`); + } +} + +export class StagehandInvalidArgumentError extends StagehandError { + constructor(message: string) { + super(`InvalidArgumentError: ${message}`); + } +} + +export class StagehandElementNotFoundError extends StagehandError { + constructor(xpaths: string[]) { + super(`Could not find an element for the given xPath(s): ${xpaths}`); + } +} + +export class AgentScreenshotProviderError extends StagehandError { + constructor(message: string) { + super(`ScreenshotProviderError: ${message}`); + } +} + +export class StagehandMissingArgumentError extends StagehandError { + constructor(message: string) { + super(`MissingArgumentError: ${message}`); + } +} + +export class CreateChatCompletionResponseError extends StagehandError { + constructor(message: string) { + super(`CreateChatCompletionResponseError: ${message}`); + } +} + +export class StagehandEvalError extends StagehandError { + constructor(message: string) { + super(`StagehandEvalError: ${message}`); + } +} + +export class StagehandDomProcessError extends StagehandError { + constructor(message: string) { + super(`Error Processing Dom: ${message}`); + } +}