From d4e7f2d23d6b8781aa4cf4d5d995919a5d083520 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 30 Jul 2025 10:29:59 -0700 Subject: [PATCH 01/20] Updated pnpm-lock.yaml --- pnpm-lock.yaml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b81dabcd6..035dfa4dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: specifier: workspace:* version: link:.. devDependencies: + jszip: + specifier: ^3.10.1 + version: 3.10.1 tsx: specifier: ^4.10.5 version: 4.19.4 @@ -3153,6 +3156,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -3464,6 +3470,9 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} @@ -3504,6 +3513,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4109,6 +4121,9 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4560,6 +4575,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -8825,6 +8843,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + immer@9.0.21: {} import-fresh@3.3.1: @@ -9111,6 +9131,13 @@ snapshots: jsonpointer@5.0.1: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwa@2.0.0: dependencies: buffer-equal-constant-time: 1.0.1 @@ -9157,6 +9184,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} linear-sum-assignment@1.0.7: @@ -10068,6 +10099,8 @@ snapshots: dependencies: quansync: 0.2.10 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -10694,6 +10727,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} sharp@0.33.5: From 56ca7a7877328db15e8f412b11eac27c4751eb70 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 30 Jul 2025 10:38:27 -0700 Subject: [PATCH 02/20] Enabled strict: true in tsconfig.json --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d97b9cb50..6466bfe24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "@/*": ["./*"] }, "skipLibCheck": true, - "declaration": true + "declaration": true, + "strict": true }, "exclude": ["node_modules", "dist", ".eslintrc.cjs"] } From 44c472e43b024e048ad31cfaae69fd09ca7798c3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 30 Jul 2025 12:47:27 -0700 Subject: [PATCH 03/20] Fixed TS errors in lib/index.ts, added notes --- lib/index.ts | 104 ++++++++++++++++++++++++++++++++------------- types/browser.ts | 2 +- types/stagehand.ts | 8 ++-- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 0bd9e7047..706280989 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -93,8 +93,8 @@ async function getBrowser( let debugUrl: string | undefined = undefined; let sessionUrl: string | undefined = undefined; - let sessionId: string; - let connectUrl: string; + let sessionId: string | undefined = undefined; + let connectUrl: string | undefined = undefined; const browserbase = new Browserbase({ apiKey, @@ -112,6 +112,12 @@ async function getBrowser( ); } + if (!session.connectUrl) { + throw new StagehandError( + `Session ${browserbaseSessionID} has no connect URL`, + ); + } + sessionId = browserbaseSessionID; connectUrl = session.connectUrl; @@ -126,18 +132,24 @@ async function getBrowser( }, }, }); - } catch (error) { + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + const errorStack = + (error instanceof Error ? error.stack : undefined) ?? + "No stack trace available"; + logger({ category: "init", message: "failed to resume session", level: 0, auxiliary: { error: { - value: error.message, + value: errorMessage, type: "string", }, trace: { - value: error.stack, + value: errorStack, type: "string", }, }, @@ -169,6 +181,7 @@ async function getBrowser( sessionId = session.id; connectUrl = session.connectUrl; + logger({ category: "init", message: "created new browserbase session", @@ -181,6 +194,7 @@ async function getBrowser( }, }); } + if (!connectUrl.includes("connect.connect")) { logger({ category: "init", @@ -314,7 +328,7 @@ async function getBrowser( context.addCookies(localBrowserLaunchOptions.cookies); } // This will always be when null launched with chromium.launchPersistentContext, but not when connected over CDP to an existing browser - const browser = context.browser(); + const browser = context.browser() ?? undefined; logger({ category: "init", @@ -369,7 +383,7 @@ export class Stagehand { private stagehandContext!: StagehandContext; public browserbaseSessionID?: string; public readonly domSettleTimeoutMs: number; - public readonly debugDom: boolean; + public readonly debugDom: boolean = false; // NOTE: This field was deprecated and isn't in the constructor params, should be deleted public readonly headless: boolean; public verbose: 0 | 1 | 2; public llmProvider: LLMProvider; @@ -378,7 +392,7 @@ export class Stagehand { private projectId: string | undefined; private externalLogger?: (logLine: LogLine) => void; private browserbaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams; - public variables: { [key: string]: unknown }; + public variables: { [key: string]: unknown } = {}; // NOTE: This field is never referenced, should be deleted private contextPath?: string; public llmClient: LLMClient; public readonly userProvidedInstructions?: string; @@ -389,10 +403,10 @@ export class Stagehand { private localBrowserLaunchOptions?: LocalBrowserLaunchOptions; public readonly selfHeal: boolean; private cleanupCalled = false; - public readonly actTimeoutMs: number; + public readonly actTimeoutMs: number = 30_000; // NOTE: This field is never used for anything but isn't deprecated, should this be deleted? public readonly logInferenceToFile?: boolean; private stagehandLogger: StagehandLogger; - private disablePino: boolean; + private disablePino: boolean; // NOTE: This field is never directly read from, should this be deleted? private modelClientOptions: ClientOptions; private _env: "LOCAL" | "BROWSERBASE"; private _browser: Browser | undefined; @@ -531,7 +545,7 @@ export class Stagehand { waitForCaptchaSolves = false, logInferenceToFile = false, selfHeal = false, - disablePino, + disablePino = false, experimental = false, }: ConstructorParams = { env: "BROWSERBASE", @@ -550,9 +564,7 @@ export class Stagehand { this.externalLogger, ); - this.enableCaching = - enableCaching ?? - (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true"); + this.enableCaching = enableCaching ?? process.env.ENABLE_CACHING === "true"; this.llmProvider = llmProvider || new LLMProvider(this.logger, this.enableCaching); @@ -581,7 +593,7 @@ export class Stagehand { this.stagehandLogger.setVerbosity(this.verbose); this.modelName = modelName ?? DEFAULT_MODEL_NAME; - let modelApiKey: string | undefined; + let modelApiKey: string | undefined = undefined; if (!modelClientOptions?.apiKey) { // If no API key is provided, try to load it from the environment @@ -592,17 +604,29 @@ export class Stagehand { ); } else { // Temporary add for legacy providers - modelApiKey = - LLMProvider.getModelProvider(this.modelName) === "openai" - ? process.env.OPENAI_API_KEY || - this.llmClient?.clientOptions?.apiKey - : LLMProvider.getModelProvider(this.modelName) === "anthropic" - ? process.env.ANTHROPIC_API_KEY || - this.llmClient?.clientOptions?.apiKey - : LLMProvider.getModelProvider(this.modelName) === "google" - ? process.env.GOOGLE_API_KEY || - this.llmClient?.clientOptions?.apiKey - : undefined; + const modelProvider = LLMProvider.getModelProvider(this.modelName); + switch (modelProvider) { + case "openai": + modelApiKey = + process.env.OPENAI_API_KEY ?? + modelClientOptions?.apiKey ?? + undefined; + break; + case "anthropic": + modelApiKey = + process.env.ANTHROPIC_API_KEY ?? + modelClientOptions?.apiKey ?? + undefined; + break; + case "google": + modelApiKey = + process.env.GOOGLE_API_KEY ?? + modelClientOptions?.apiKey ?? + undefined; + break; + default: + modelApiKey = undefined; + } } this.modelClientOptions = { ...modelClientOptions, @@ -628,13 +652,20 @@ export class Stagehand { this.modelClientOptions, ); } catch (error) { + // NOTE: I added the code to log and rethrow the error if it's not one of these instances, + // but I don't understand why we would try to proceed from failing to initialize the LLM + // client ever. Assuming we shouldn't, it makes more sense to remove the if statement or + // remove the try/catch completely. if ( error instanceof UnsupportedAISDKModelProviderError || error instanceof InvalidAISDKModelFormatError ) { throw error; } - this.llmClient = undefined; + console.error( + `Unexpected error while initializing LLM client: ${error}`, + ); + throw error; } } @@ -749,6 +780,13 @@ export class Stagehand { } if (this.usingAPI) { + // NOTE: This should theoretically never trigger because of the constructor, but this is safe + if (!this.apiKey || !this.projectId) { + throw new StagehandInitError( + "API mode requires both apiKey and projectId to be defined", + ); + } + this.apiClient = new StagehandAPI({ apiKey: this.apiKey, projectId: this.projectId, @@ -756,6 +794,11 @@ export class Stagehand { }); const modelApiKey = this.modelClientOptions?.apiKey; + if (!modelApiKey) { + throw new StagehandInitError( + `API mode requires a model API key for ${this.modelName} to be defined`, + ); + } const { sessionId, available } = await this.apiClient.init({ modelName: this.modelName, modelApiKey: modelApiKey, @@ -770,7 +813,7 @@ export class Stagehand { browserbaseSessionID: this.browserbaseSessionID, }); if (!available) { - this.apiClient = null; + this.apiClient = undefined; } this.browserbaseSessionID = sessionId; } @@ -863,7 +906,7 @@ export class Stagehand { throw new StagehandError((body as ErrorResponse).message); } } - this.apiClient = null; + this.apiClient = undefined; return; } else { await this.context.close(); @@ -911,7 +954,8 @@ export class Stagehand { instructionOrOptions: string | AgentExecuteOptions, ) => Promise; } { - if (!options || !options.provider) { + // NOTE: If AgentConfig matched the docs, we would only need to check !options (see AgentConfig definition) + if (!options || !options.provider || !options.model) { // use open operator agent return { execute: async (instructionOrOptions: string | AgentExecuteOptions) => { diff --git a/types/browser.ts b/types/browser.ts index 3a7779668..d15075dd9 100644 --- a/types/browser.ts +++ b/types/browser.ts @@ -3,7 +3,7 @@ import { Browser, BrowserContext } from "./page"; export interface BrowserResult { env: "LOCAL" | "BROWSERBASE"; browser?: Browser; - context: BrowserContext; + context?: BrowserContext; debugUrl?: string; sessionUrl?: string; contextPath?: string; diff --git a/types/stagehand.ts b/types/stagehand.ts index 706a45a3b..eaf628de5 100644 --- a/types/stagehand.ts +++ b/types/stagehand.ts @@ -103,9 +103,9 @@ export interface ConstructorParams { } export interface InitResult { - debugUrl: string; - sessionUrl: string; - sessionId: string; + debugUrl?: string; + sessionUrl?: string; + sessionId?: string; } export interface ActOptions { @@ -256,6 +256,8 @@ export interface AgentExecuteParams { context?: string; } +// NOTE: This contradicts the docs, which say that provider and model are required +// https://docs.stagehand.dev/reference/agent#arguments%3A-agentoptions /** * Configuration for agent functionality */ From fd6c2d7475557d54b9413c6fb24f9de7c6080464 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 12:28:31 -0700 Subject: [PATCH 04/20] Used guarantee that StagehandPage will always receive an LLMClient (this contradicts #379) --- lib/StagehandPage.ts | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 80387d134..297581399 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -114,25 +114,23 @@ export class StagehandPage { this.userProvidedInstructions = userProvidedInstructions; this.waitForCaptchaSolves = waitForCaptchaSolves ?? false; - if (this.llmClient) { - this.actHandler = new StagehandActHandler({ - logger: this.stagehand.logger, - stagehandPage: this, - selfHeal: this.stagehand.selfHeal, - }); - this.extractHandler = new StagehandExtractHandler({ - stagehand: this.stagehand, - logger: this.stagehand.logger, - stagehandPage: this, - userProvidedInstructions, - }); - this.observeHandler = new StagehandObserveHandler({ - stagehand: this.stagehand, - logger: this.stagehand.logger, - stagehandPage: this, - userProvidedInstructions, - }); - } + this.actHandler = new StagehandActHandler({ + logger: this.stagehand.logger, + stagehandPage: this, + selfHeal: this.stagehand.selfHeal, + }); + this.extractHandler = new StagehandExtractHandler({ + stagehand: this.stagehand, + logger: this.stagehand.logger, + stagehandPage: this, + userProvidedInstructions, + }); + this.observeHandler = new StagehandObserveHandler({ + stagehand: this.stagehand, + logger: this.stagehand.logger, + stagehandPage: this, + userProvidedInstructions, + }); } public ordinalForFrameId(fid: string | undefined): number { From d250aa3693c93b8c361f6bc4bf90457fceeb2e63 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 13:52:52 -0700 Subject: [PATCH 05/20] Fixes all but one TS error in StagehandPage.ts --- lib/StagehandPage.ts | 56 ++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 297581399..e599cfa1e 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -47,7 +47,7 @@ export class StagehandPage { private observeHandler: StagehandObserveHandler; private llmClient: LLMClient; private cdpClient: CDPSession | null = null; - private api: StagehandAPI; + private api?: StagehandAPI; private userProvidedInstructions?: string; private waitForCaptchaSolves: boolean; private initialized: boolean = false; @@ -101,7 +101,8 @@ export class StagehandPage { const value = target[prop]; // If the property is a function, wrap it to update active page before execution if (typeof value === "function" && prop !== "on") { - return (...args: unknown[]) => value.apply(target, args); + return (...args: unknown[]) => + (value as (...a: unknown[]) => unknown).apply(target, args); } return value; }, @@ -178,7 +179,10 @@ ${scriptContent} \ level: 1, auxiliary: { error: { value: (err as Error).message, type: "string" }, - trace: { value: (err as Error).stack, type: "string" }, + trace: { + value: (err as Error).stack ?? "No stack trace available", + type: "string", + }, }, }); throw err; @@ -278,17 +282,18 @@ ${scriptContent} \ } // Use type assertion to safely call the method with proper typing + type EnhancedOptions = + | ActOptions + | ExtractOptions + | ObserveOptions; type EnhancedMethod = ( - options: - | ActOptions - | ExtractOptions - | ObserveOptions, + options: EnhancedOptions, ) => Promise< ActResult | ExtractResult | ObserveResult[] >; const method = this[prop as keyof StagehandPage] as EnhancedMethod; - return (options: unknown) => method.call(this, options); + return (options: EnhancedOptions) => method.call(this, options); } // Handle screenshots with CDP @@ -393,7 +398,8 @@ ${scriptContent} \ // For all other method calls, update active page if (typeof value === "function") { - return (...args: unknown[]) => value.apply(target, args); + return (...args: unknown[]) => + (value as (...a: unknown[]) => unknown).apply(target, args); } return value; @@ -700,15 +706,19 @@ ${scriptContent} \ await clearOverlays(this.page); - // check if user called extract() with no arguments - if (!instructionOrOptions) { + // check if user called extract() with no arguments or empty string + // NOTE: This explicitly matches the old behavior of early exiting on empty string, + // but it might make more sense to only early exit on undefined. + if (instructionOrOptions === undefined || instructionOrOptions === "") { let result: ExtractResult; if (this.api) { result = await this.api.extract({ frameId: this.rootFrameId }); } else { result = await this.extractHandler.extract(); } - this.stagehand.addToHistory("extract", instructionOrOptions, result); + // NOTE: This always pushes empty string into the history because of `addToHistory`'s + // type signature, feel free to change this to something else. + this.stagehand.addToHistory("extract", "", result); return result; } @@ -716,17 +726,19 @@ ${scriptContent} \ typeof instructionOrOptions === "string" ? { instruction: instructionOrOptions, - schema: defaultExtractSchema as T, + schema: defaultExtractSchema as unknown as T, } : instructionOrOptions.schema ? instructionOrOptions : { ...instructionOrOptions, - schema: defaultExtractSchema as T, + schema: defaultExtractSchema as unknown as T, }; + // NOTE: The current code early exits on empty string, but proceeds with { instruction: "" } + // This is a bit confusing, but it's consistent with the old behavior. const { - instruction, + instruction = "", schema, modelName, modelClientOptions, @@ -830,7 +842,7 @@ ${scriptContent} \ : instructionOrOptions || {}; const { - instruction, + instruction = "", modelName, modelClientOptions, domSettleTimeoutMs, @@ -843,7 +855,11 @@ ${scriptContent} \ if (this.api) { const opts = { ...options, frameId: this.rootFrameId }; const result = await this.api.observe(opts); - this.stagehand.addToHistory("observe", instructionOrOptions, result); + this.stagehand.addToHistory( + "observe", + instructionOrOptions ?? "", + result, + ); return result; } @@ -921,7 +937,11 @@ ${scriptContent} \ throw e; }); - this.stagehand.addToHistory("observe", instructionOrOptions, result); + this.stagehand.addToHistory( + "observe", + instructionOrOptions ?? "", + result, + ); return result; } catch (err: unknown) { From 0dcaf0c36544d558f163c43913103fa534173911 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 15:07:27 -0700 Subject: [PATCH 06/20] Fixed all TS errors in observeHandler.ts --- lib/handlers/observeHandler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/handlers/observeHandler.ts b/lib/handlers/observeHandler.ts index c14e4d6ea..a44e8ad17 100644 --- a/lib/handlers/observeHandler.ts +++ b/lib/handlers/observeHandler.ts @@ -1,5 +1,5 @@ import { LogLine } from "../../types/log"; -import { Stagehand, StagehandFunctionName } from "../index"; +import { ObserveResult, Stagehand, StagehandFunctionName } from "../index"; import { observe } from "../inference"; import { LLMClient } from "../llm/LLMClient"; import { StagehandPage } from "../StagehandPage"; @@ -55,7 +55,7 @@ export class StagehandObserveHandler { drawOverlay?: boolean; fromAct?: boolean; iframes?: boolean; - }) { + }): Promise { if (!instruction) { instruction = `Find elements that can be used for any future actions in the page. These may be navigation links, related pages, section/subsection links, buttons, or other interactive elements. Be comprehensive: if there are multiple elements that may be relevant for future actions, return all of them.`; } @@ -131,7 +131,7 @@ export class StagehandObserveHandler { ); //Add iframes to the observation response if there are any on the page - if (discoveredIframes.length > 0) { + if (discoveredIframes && discoveredIframes.length > 0) { this.logger({ category: "observation", message: `Warning: found ${discoveredIframes.length} iframe(s) on the page. If you wish to interact with iframe content, please make sure you are setting iframes: true`, @@ -151,7 +151,7 @@ export class StagehandObserveHandler { }); } - const elementsWithSelectors = ( + const elementsWithSelectors: ObserveResult[] = ( await Promise.all( observationResponse.elements.map(async (element) => { const { elementId, ...rest } = element; From b8a81a91026ab1e8cdcc4b1f1bccb8d1767e3326 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 16:48:24 -0700 Subject: [PATCH 07/20] Fixed most TS issues in api.ts --- lib/api.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/api.ts b/lib/api.ts index de60d2b8d..432edebc1 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -34,7 +34,7 @@ export class StagehandAPI { private apiKey: string; private projectId: string; private sessionId?: string; - private modelApiKey: string; + private modelApiKey?: string; private logger: (message: LogLine) => void; private fetchWithCookies; @@ -65,6 +65,8 @@ export class StagehandAPI { this.modelApiKey = modelApiKey; const region = browserbaseSessionCreateParams?.region; + // NOTE: Looks like there was some discussion here -- I personally agree that this should throw + // Thread: https://github.com/browserbase/stagehand/pull/801#discussion_r2138856174 if (region && region !== "us-west-2") { return { sessionId: browserbaseSessionID ?? null, available: false }; } @@ -165,6 +167,8 @@ export class StagehandAPI { return response; } + // NOTE: This is a strange way to process SSE, and also doesn't guarantee that it returns T (actually explicitly returns null in one case) + // I feel pretty confused by this, so I'll leave this here to refactor later private async execute({ method, args, @@ -237,6 +241,14 @@ export class StagehandAPI { path: string, options: RequestInit = {}, ): Promise { + // These must be set for a request to be made successfully + if (!this.modelApiKey) { + throw new StagehandAPIError("modelApiKey is required"); + } + if (!this.sessionId) { + throw new StagehandAPIError("sessionId is required"); + } + const defaultHeaders: Record = { "x-bb-api-key": this.apiKey, "x-bb-project-id": this.projectId, From 3ec67ea3848a913542cef191d1a29bed6e6a361c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 16:56:51 -0700 Subject: [PATCH 08/20] Fixed TS errors in LLMClient.ts by marking type, hasVision, and clientOptions as abstract --- lib/llm/LLMClient.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/llm/LLMClient.ts b/lib/llm/LLMClient.ts index 524a71896..b10cafe8f 100644 --- a/lib/llm/LLMClient.ts +++ b/lib/llm/LLMClient.ts @@ -97,10 +97,15 @@ export interface CreateChatCompletionOptions { } export abstract class LLMClient { - public type: "openai" | "anthropic" | "cerebras" | "groq" | (string & {}); + public abstract type: + | "openai" + | "anthropic" + | "cerebras" + | "groq" + | (string & {}); public modelName: AvailableModel | (string & {}); - public hasVision: boolean; - public clientOptions: ClientOptions; + public abstract hasVision: boolean; + public abstract clientOptions: ClientOptions; public userProvidedInstructions?: string; constructor(modelName: AvailableModel, userProvidedInstructions?: string) { From e8722e891ab607b733b3eac1c177d7f84d784c4b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 16:59:39 -0700 Subject: [PATCH 09/20] Updated logger type to LogLine in LLMCache.ts --- lib/cache/LLMCache.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/cache/LLMCache.ts b/lib/cache/LLMCache.ts index 53bd78213..41236837b 100644 --- a/lib/cache/LLMCache.ts +++ b/lib/cache/LLMCache.ts @@ -1,12 +1,9 @@ +import { LogLine } from "../../types/log"; import { BaseCache, CacheEntry } from "./BaseCache"; export class LLMCache extends BaseCache { constructor( - logger: (message: { - category?: string; - message: string; - level?: number; - }) => void, + logger: (message: LogLine) => void, cacheDir?: string, cacheFile?: string, ) { From 54fa3f797d67c63542e7be6a9601d812600d17f1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 17:09:26 -0700 Subject: [PATCH 10/20] Fixed TS typing issues in LLMProvider.ts --- lib/llm/LLMProvider.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/llm/LLMProvider.ts b/lib/llm/LLMProvider.ts index 4c7e9f1d8..7ec0b13ed 100644 --- a/lib/llm/LLMProvider.ts +++ b/lib/llm/LLMProvider.ts @@ -9,6 +9,8 @@ import { ClientOptions, ModelProvider, } from "../../types/model"; +import { ClientOptions as OpenAIClientOptions } from "openai"; +import { ClientOptions as AnthropicClientOptions } from "@anthropic-ai/sdk"; import { LLMCache } from "../cache/LLMCache"; import { AISdkClient } from "./aisdk"; import { AnthropicClient } from "./AnthropicClient"; @@ -150,7 +152,7 @@ export class LLMProvider { }, }, }); - this.cache.deleteCacheForRequestId(requestId); + this.cache?.deleteCacheForRequestId(requestId); } getClient( @@ -165,7 +167,7 @@ export class LLMProvider { const languageModel = getAISDKLanguageModel( subProvider, subModelName, - clientOptions?.apiKey, + clientOptions?.apiKey ?? undefined, ); return new AISdkClient({ @@ -188,7 +190,7 @@ export class LLMProvider { enableCaching: this.enableCaching, cache: this.cache, modelName: availableModel, - clientOptions, + clientOptions: clientOptions as OpenAIClientOptions, }); case "anthropic": return new AnthropicClient({ @@ -196,7 +198,7 @@ export class LLMProvider { enableCaching: this.enableCaching, cache: this.cache, modelName: availableModel, - clientOptions, + clientOptions: clientOptions as AnthropicClientOptions, }); case "cerebras": return new CerebrasClient({ @@ -204,7 +206,7 @@ export class LLMProvider { enableCaching: this.enableCaching, cache: this.cache, modelName: availableModel, - clientOptions, + clientOptions: clientOptions as OpenAIClientOptions, }); case "groq": return new GroqClient({ @@ -212,7 +214,7 @@ export class LLMProvider { enableCaching: this.enableCaching, cache: this.cache, modelName: availableModel, - clientOptions, + clientOptions: clientOptions as OpenAIClientOptions, }); case "google": return new GoogleClient({ From 2c2ccb5788a19a1a02ba5309c3e89da03aab73aa Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 17:31:09 -0700 Subject: [PATCH 11/20] Fixed some TS errors in extractHandler.ts --- lib/handlers/extractHandler.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/handlers/extractHandler.ts b/lib/handlers/extractHandler.ts index 6e3b614bd..19f1908ce 100644 --- a/lib/handlers/extractHandler.ts +++ b/lib/handlers/extractHandler.ts @@ -26,12 +26,7 @@ export class StagehandExtractHandler { userProvidedInstructions, }: { stagehand: Stagehand; - logger: (message: { - category?: string; - message: string; - level?: number; - auxiliary?: { [key: string]: { value: string; type: string } }; - }) => void; + logger: (logLine: LogLine) => void; stagehandPage: StagehandPage; userProvidedInstructions?: string; }) { @@ -154,7 +149,7 @@ export class StagehandExtractHandler { combinedTree, combinedUrlMap, combinedXpathMap: {} as Record, - discoveredIframes: [] as undefined, + discoveredIframes: [], })) : getAccessibilityTree(this.stagehandPage, this.logger, targetXpath).then( ({ simplified, idToUrl, iframes: frameNodes }) => ({ From c2d02af97b44746c19f2912ed2188ee892594338 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 17:31:45 -0700 Subject: [PATCH 12/20] Implemented overloaded function signatures for extract with more specific typing for extractHandler.ts --- lib/handlers/extractHandler.ts | 55 +++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/handlers/extractHandler.ts b/lib/handlers/extractHandler.ts index 19f1908ce..7b2c1bf7c 100644 --- a/lib/handlers/extractHandler.ts +++ b/lib/handlers/extractHandler.ts @@ -36,30 +36,33 @@ export class StagehandExtractHandler { this.userProvidedInstructions = userProvidedInstructions; } - public async extract({ - instruction, - schema, - content = {}, - llmClient, - requestId, - domSettleTimeoutMs, - useTextExtract, - selector, - iframes, - }: { - instruction?: string; - schema?: T; + public async extract(): Promise<{ page_text?: string }>; + public async extract(args: { + instruction: string; + schema: T; content?: z.infer; - chunksSeen?: Array; - llmClient?: LLMClient; - requestId?: string; + chunksSeen?: Array; // NOTE: This is unused, seems like this should be deleted + llmClient: LLMClient; + requestId: string; domSettleTimeoutMs?: number; useTextExtract?: boolean; selector?: string; iframes?: boolean; - } = {}): Promise> { - const noArgsCalled = !instruction && !schema && !llmClient && !selector; - if (noArgsCalled) { + }): Promise>; + + public async extract(args?: { + instruction: string; + schema: T; + content?: z.infer; + chunksSeen?: Array; // NOTE: This is unused, seems like this should be deleted + llmClient: LLMClient; + requestId: string; + domSettleTimeoutMs?: number; + useTextExtract?: boolean; + selector?: string; + iframes?: boolean; + }): Promise | { page_text?: string }> { + if (!args) { this.logger({ category: "extraction", message: "Extracting the entire page text.", @@ -68,6 +71,18 @@ export class StagehandExtractHandler { return this.extractPageText(); } + const { + instruction, + schema, + content = {}, + llmClient, + requestId, + domSettleTimeoutMs, + useTextExtract, + selector, + iframes, + } = args; + if (useTextExtract !== undefined) { this.logger({ category: "extraction", @@ -117,7 +132,7 @@ export class StagehandExtractHandler { schema: T; content?: z.infer; llmClient: LLMClient; - requestId?: string; + requestId: string; domSettleTimeoutMs?: number; selector?: string; iframes?: boolean; From 0a4d1279a0277679aa4748f3a5ff3c65ae6293ad Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 31 Jul 2025 17:52:03 -0700 Subject: [PATCH 13/20] Added note to ExtractOptions interface definition and added tempfix + note to StagehandPage.ts --- lib/StagehandPage.ts | 4 +++- types/stagehand.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index e599cfa1e..cbb5edbb8 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -737,9 +737,11 @@ ${scriptContent} \ // NOTE: The current code early exits on empty string, but proceeds with { instruction: "" } // This is a bit confusing, but it's consistent with the old behavior. + // NOTE: instruction and schema should be defined at this point as per the docs, see ExtractOptions interface definition + // The below default values should be removed in the future const { instruction = "", - schema, + schema = defaultExtractSchema, modelName, modelClientOptions, domSettleTimeoutMs, diff --git a/types/stagehand.ts b/types/stagehand.ts index eaf628de5..fb10365d8 100644 --- a/types/stagehand.ts +++ b/types/stagehand.ts @@ -125,6 +125,8 @@ export interface ActResult { action: string; } +// NOTE: This contradicts the docs, which say that instruction and schema are required +// https://docs.stagehand.dev/reference/extract#arguments%3A-extractoptions%3Ct-extends-z-anyzodobject%3E export interface ExtractOptions { instruction?: string; schema?: T; From 650addd019174875a23529d899360fa8cb2b8ebc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 07:09:38 -0700 Subject: [PATCH 14/20] Removed Zod type inference for ObserveResponse to define a more specific type --- lib/inference.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/inference.ts b/lib/inference.ts index c411308e8..818c78669 100644 --- a/lib/inference.ts +++ b/lib/inference.ts @@ -289,7 +289,14 @@ export async function observe({ .describe("an array of accessible elements that match the instruction"), }); - type ObserveResponse = z.infer; + type ObserveResponse = { + elements: { + elementId: string; + description: string; + method?: string; + arguments?: string[]; + }[]; + }; const messages: ChatMessage[] = [ buildObserveSystemPrompt(userProvidedInstructions), From 2a9d633abe14769f8d89c55c764745912241db33 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 07:10:07 -0700 Subject: [PATCH 15/20] Left note about remaining skipped TS errors in inference.ts --- lib/inference.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/inference.ts b/lib/inference.ts index 818c78669..e7e376e10 100644 --- a/lib/inference.ts +++ b/lib/inference.ts @@ -177,6 +177,9 @@ export async function extract({ }); const metadataEndTime = Date.now(); + // NOTE: This is suspicious -- analyzed statically, this shouldn't work at all: + // I suspect llmClient.createChatCompletion's return type in TS is wrong, so + // I'm won't mess with this yet (same for all other LLMParsedResponse usages) const { data: { completed: metadataResponseCompleted, From 9ce37fb40e4fc29f8ee1f9f33a30f50f9becd544 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 07:41:42 -0700 Subject: [PATCH 16/20] Fixed TS errors in actHandler.ts --- lib/handlers/actHandler.ts | 61 ++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/lib/handlers/actHandler.ts b/lib/handlers/actHandler.ts index ba260b5d9..6e0912cb9 100644 --- a/lib/handlers/actHandler.ts +++ b/lib/handlers/actHandler.ts @@ -65,8 +65,11 @@ export class StagehandActHandler { }, }); + // NOTE: This function seems to assume `method` is always defined, but + // the function signature doesn't guarantee that and no checks are made + // Feel free to extract this case into a different error message const method = observe.method; - if (method === "not-supported") { + if (!method || method === "not-supported") { this.logger({ category: "action", message: "Cannot execute ObserveResult with unsupported method", @@ -107,6 +110,13 @@ export class StagehandActHandler { action: observe.description || `ObserveResult action (${method})`, }; } catch (err) { + // NOTE: I've used this pattern because it's technically safer, but I duplicated this a lot + // Feel free to extract this to a helper function or just use TS assertions to mute + // the error messages, I'll leave that to you + const errorMessage = err instanceof Error ? err.message : String(err); + const errorStack = + (err instanceof Error ? err.stack : undefined) ?? + "No stack trace available"; if ( !this.selfHeal || err instanceof PlaywrightCommandMethodNotSupportedException @@ -116,13 +126,16 @@ export class StagehandActHandler { message: "Error performing act from an ObserveResult", level: 1, auxiliary: { - error: { value: err.message, type: "string" }, - trace: { value: err.stack, type: "string" }, + error: { value: errorMessage, type: "string" }, + trace: { + value: errorStack, + type: "string", + }, }, }); return { success: false, - message: `Failed to perform act: ${err.message}`, + message: `Failed to perform act: ${errorMessage}`, action: observe.description || `ObserveResult action (${method})`, }; } @@ -133,8 +146,11 @@ export class StagehandActHandler { "Error performing act from an ObserveResult. Reprocessing the page and trying again", level: 1, auxiliary: { - error: { value: err.message, type: "string" }, - trace: { value: err.stack, type: "string" }, + error: { value: errorMessage, type: "string" }, + trace: { + value: errorStack, + type: "string", + }, observeResult: { value: JSON.stringify(observe), type: "object" }, }, }); @@ -165,8 +181,8 @@ export class StagehandActHandler { const element: ObserveResult = observeResults[0]; await this._performPlaywrightMethod( // override previously provided method and arguments - observe.method, - observe.arguments, + method, + args, // only update selector element.selector, domSettleTimeoutMs, @@ -177,18 +193,28 @@ export class StagehandActHandler { action: observe.description || `ObserveResult action (${method})`, }; } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + const errorStack = + (err instanceof Error ? err.stack : undefined) ?? + "No stack trace available"; this.logger({ category: "action", message: "Error performing act from an ObserveResult on fallback", level: 1, auxiliary: { - error: { value: err.message, type: "string" }, - trace: { value: err.stack, type: "string" }, + error: { + value: errorMessage, + type: "string", + }, + trace: { + value: errorStack, + type: "string", + }, }, }); return { success: false, - message: `Failed to perform act: ${err.message}`, + message: `Failed to perform act: ${err instanceof Error ? err.message : String(err)}`, action: observe.description || `ObserveResult action (${method})`, }; } @@ -270,7 +296,7 @@ export class StagehandActHandler { if (actionOrOptions.variables) { Object.keys(actionOrOptions.variables).forEach((key) => { - element.arguments = element.arguments.map((arg) => + element.arguments = element.arguments?.map((arg) => arg.replace(`%${key}%`, actionOrOptions.variables![key]), ); }); @@ -365,19 +391,24 @@ export class StagehandActHandler { // Always wait for DOM to settle await this.stagehandPage._waitForSettledDom(domSettleTimeoutMs); } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + const errorStack = + (e instanceof Error ? e.stack : undefined) ?? + "No stack trace available"; + this.logger({ category: "action", message: "error performing method", level: 1, auxiliary: { - error: { value: e.message, type: "string" }, - trace: { value: e.stack, type: "string" }, + error: { value: errorMessage, type: "string" }, + trace: { value: errorStack, type: "string" }, method: { value: method, type: "string" }, xpath: { value: xpath, type: "string" }, args: { value: JSON.stringify(args), type: "object" }, }, }); - throw new PlaywrightCommandException(e.message); + throw new PlaywrightCommandException(errorMessage); } } } From b6c9df3d6ac3fe74246a6cfb01037ae56cae3cd0 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 07:42:44 -0700 Subject: [PATCH 17/20] Refactored unnecessary Promise.all on async callbacks in observeHandler.ts --- lib/handlers/observeHandler.ts | 110 ++++++++++++++++----------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/lib/handlers/observeHandler.ts b/lib/handlers/observeHandler.ts index a44e8ad17..e9215eb58 100644 --- a/lib/handlers/observeHandler.ts +++ b/lib/handlers/observeHandler.ts @@ -151,67 +151,67 @@ export class StagehandObserveHandler { }); } - const elementsWithSelectors: ObserveResult[] = ( - await Promise.all( - observationResponse.elements.map(async (element) => { - const { elementId, ...rest } = element; - - // Generate xpath for the given element if not found in selectorMap - this.logger({ - category: "observation", - message: "Getting xpath for element", - level: 1, - auxiliary: { - elementId: { - value: elementId.toString(), - type: "string", - }, + // NOTE: Not sure why this was a Promise.all? Feel free to keep if you want the parallelization, + // but I think it's unnecessary and can be removed + const elementsWithSelectors: ObserveResult[] = observationResponse.elements + .map((element) => { + const { elementId, ...rest } = element; + + // Generate xpath for the given element if not found in selectorMap + this.logger({ + category: "observation", + message: "Getting xpath for element", + level: 1, + auxiliary: { + elementId: { + value: elementId.toString(), + type: "string", }, - }); + }, + }); - if (elementId.includes("-")) { - const lookUpIndex = elementId as EncodedId; - const xpath: string | undefined = combinedXpathMap[lookUpIndex]; - - const trimmedXpath = trimTrailingTextNode(xpath); - - if (!trimmedXpath || trimmedXpath === "") { - this.logger({ - category: "observation", - message: `Empty xpath returned for element`, - auxiliary: { - observeResult: { - value: JSON.stringify(element), - type: "object", - }, - }, - level: 1, - }); - return undefined; - } - - return { - ...rest, - selector: `xpath=${trimmedXpath}`, - // Provisioning or future use if we want to use direct CDP - // backendNodeId: elementId, - }; - } else { + if (elementId.includes("-")) { + const lookUpIndex = elementId as EncodedId; + const xpath: string | undefined = combinedXpathMap[lookUpIndex]; + + const trimmedXpath = trimTrailingTextNode(xpath); + + if (!trimmedXpath || trimmedXpath === "") { this.logger({ category: "observation", - message: `Element is inside a shadow DOM: ${elementId}`, - level: 0, + message: `Empty xpath returned for element`, + auxiliary: { + observeResult: { + value: JSON.stringify(element), + type: "object", + }, + }, + level: 1, }); - return { - description: "an element inside a shadow DOM", - method: "not-supported", - arguments: [] as string[], - selector: "not-supported", - }; + return undefined; } - }), - ) - ).filter((e: T | undefined): e is T => e !== undefined); + + return { + ...rest, + selector: `xpath=${trimmedXpath}`, + // Provisioning or future use if we want to use direct CDP + // backendNodeId: elementId, + }; + } else { + this.logger({ + category: "observation", + message: `Element is inside a shadow DOM: ${elementId}`, + level: 0, + }); + return { + description: "an element inside a shadow DOM", + method: "not-supported", + arguments: [] as string[], + selector: "not-supported", + }; + } + }) + .filter((e: T | undefined): e is T => e !== undefined); this.logger({ category: "observation", From 23ed7c143e037fd999e7a9e0f8201eae156e40d9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 07:44:18 -0700 Subject: [PATCH 18/20] Fixed a few TS errors in operatorHandler.ts --- lib/handlers/operatorHandler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/handlers/operatorHandler.ts b/lib/handlers/operatorHandler.ts index 61ab5616d..58116deb3 100644 --- a/lib/handlers/operatorHandler.ts +++ b/lib/handlers/operatorHandler.ts @@ -30,6 +30,7 @@ export class StagehandOperatorHandler { this.stagehandPage = stagehandPage; this.logger = logger; this.llmClient = llmClient; + this.messages = []; } public async execute( @@ -74,7 +75,7 @@ export class StagehandOperatorHandler { let result: string = ""; if (action.type === "act") { const args = action.playwrightArguments as ObserveResult; - result = `Performed a "${args.method}" action ${args.arguments.length > 0 ? `with arguments: ${args.arguments.map((arg) => `"${arg}"`).join(", ")}` : ""} on "${args.description}"`; + result = `Performed a "${args.method}" action ${args.arguments && args.arguments.length > 0 ? `with arguments: ${args.arguments.map((arg) => `"${arg}"`).join(", ")}` : ""} on "${args.description}"`; } else if (action.type === "extract") { result = `Extracted data: ${action.extractionResult}`; } From aa706d02253009bc45fd9a33e0dc4f464bdb95b8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 1 Aug 2025 08:09:17 -0700 Subject: [PATCH 19/20] Fixed some TS errors in utils.ts --- lib/a11y/utils.ts | 9 +++++++-- types/context.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/a11y/utils.ts b/lib/a11y/utils.ts index b93d99720..3f4a1fccf 100644 --- a/lib/a11y/utils.ts +++ b/lib/a11y/utils.ts @@ -206,7 +206,11 @@ export async function buildBackendIdMaps( xpathMap[enc] = path; // recurse into sub-document if