@browserbasehq/stagehand 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.ts ADDED
@@ -0,0 +1,900 @@
1
+ import { Browserbase } from "@browserbasehq/sdk";
2
+ import { type BrowserContext, chromium, type Page } from "@playwright/test";
3
+ import { randomUUID } from "crypto";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { z } from "zod";
8
+ import { BrowserResult } from "../types/browser";
9
+ import { LogLine } from "../types/log";
10
+ import {
11
+ ActOptions,
12
+ ActResult,
13
+ ConstructorParams,
14
+ ExtractOptions,
15
+ ExtractResult,
16
+ InitFromPageOptions,
17
+ InitFromPageResult,
18
+ InitOptions,
19
+ InitResult,
20
+ ObserveOptions,
21
+ ObserveResult,
22
+ } from "../types/stagehand";
23
+ import { scriptContent } from "./dom/build/scriptContent";
24
+ import { StagehandActHandler } from "./handlers/actHandler";
25
+ import { StagehandExtractHandler } from "./handlers/extractHandler";
26
+ import { StagehandObserveHandler } from "./handlers/observeHandler";
27
+ import { LLMClient } from "./llm/LLMClient";
28
+ import { LLMProvider } from "./llm/LLMProvider";
29
+ import { logLineToString } from "./utils";
30
+
31
+ require("dotenv").config({ path: ".env" });
32
+
33
+ const DEFAULT_MODEL_NAME = "gpt-4o";
34
+
35
+ async function getBrowser(
36
+ apiKey: string | undefined,
37
+ projectId: string | undefined,
38
+ env: "LOCAL" | "BROWSERBASE" = "LOCAL",
39
+ headless: boolean = false,
40
+ logger: (message: LogLine) => void,
41
+ browserbaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams,
42
+ browserbaseResumeSessionID?: string,
43
+ ): Promise<BrowserResult> {
44
+ if (env === "BROWSERBASE") {
45
+ if (!apiKey) {
46
+ logger({
47
+ category: "init",
48
+ message:
49
+ "BROWSERBASE_API_KEY is required to use BROWSERBASE env. Defaulting to LOCAL.",
50
+ level: 0,
51
+ });
52
+ env = "LOCAL";
53
+ }
54
+ if (!projectId) {
55
+ logger({
56
+ category: "init",
57
+ message:
58
+ "BROWSERBASE_PROJECT_ID is required for some Browserbase features that may not work without it.",
59
+ level: 1,
60
+ });
61
+ }
62
+ }
63
+
64
+ if (env === "BROWSERBASE") {
65
+ if (!apiKey) {
66
+ throw new Error("BROWSERBASE_API_KEY is required.");
67
+ }
68
+
69
+ let debugUrl: string | undefined = undefined;
70
+ let sessionUrl: string | undefined = undefined;
71
+ let sessionId: string;
72
+ let connectUrl: string;
73
+
74
+ const browserbase = new Browserbase({
75
+ apiKey,
76
+ });
77
+
78
+ if (browserbaseResumeSessionID) {
79
+ // Validate the session status
80
+ try {
81
+ const sessionStatus = await browserbase.sessions.retrieve(
82
+ browserbaseResumeSessionID,
83
+ );
84
+
85
+ if (sessionStatus.status !== "RUNNING") {
86
+ throw new Error(
87
+ `Session ${browserbaseResumeSessionID} is not running (status: ${sessionStatus.status})`,
88
+ );
89
+ }
90
+
91
+ sessionId = browserbaseResumeSessionID;
92
+ connectUrl = `wss://connect.browserbase.com?apiKey=${apiKey}&sessionId=${sessionId}`;
93
+
94
+ logger({
95
+ category: "init",
96
+ message: "resuming existing browserbase session...",
97
+ level: 1,
98
+ auxiliary: {
99
+ sessionId: {
100
+ value: sessionId,
101
+ type: "string",
102
+ },
103
+ },
104
+ });
105
+ } catch (error) {
106
+ logger({
107
+ category: "init",
108
+ message: "failed to resume session",
109
+ level: 1,
110
+ auxiliary: {
111
+ error: {
112
+ value: error.message,
113
+ type: "string",
114
+ },
115
+ trace: {
116
+ value: error.stack,
117
+ type: "string",
118
+ },
119
+ },
120
+ });
121
+ throw error;
122
+ }
123
+ } else {
124
+ // Create new session (existing code)
125
+ logger({
126
+ category: "init",
127
+ message: "creating new browserbase session...",
128
+ level: 0,
129
+ });
130
+
131
+ if (!projectId) {
132
+ throw new Error(
133
+ "BROWSERBASE_PROJECT_ID is required for new Browserbase sessions.",
134
+ );
135
+ }
136
+
137
+ const session = await browserbase.sessions.create({
138
+ projectId,
139
+ ...browserbaseSessionCreateParams,
140
+ });
141
+
142
+ sessionId = session.id;
143
+ connectUrl = session.connectUrl;
144
+ logger({
145
+ category: "init",
146
+ message: "created new browserbase session",
147
+ level: 1,
148
+ auxiliary: {
149
+ sessionId: {
150
+ value: sessionId,
151
+ type: "string",
152
+ },
153
+ },
154
+ });
155
+ }
156
+
157
+ const browser = await chromium.connectOverCDP(connectUrl);
158
+ const { debuggerUrl } = await browserbase.sessions.debug(sessionId);
159
+
160
+ debugUrl = debuggerUrl;
161
+ sessionUrl = `https://www.browserbase.com/sessions/${sessionId}`;
162
+
163
+ logger({
164
+ category: "init",
165
+ message: browserbaseResumeSessionID
166
+ ? "browserbase session resumed"
167
+ : "browserbase session started",
168
+ level: 0,
169
+ auxiliary: {
170
+ sessionUrl: {
171
+ value: sessionUrl,
172
+ type: "string",
173
+ },
174
+ debugUrl: {
175
+ value: debugUrl,
176
+ type: "string",
177
+ },
178
+ sessionId: {
179
+ value: sessionId,
180
+ type: "string",
181
+ },
182
+ },
183
+ });
184
+
185
+ const context = browser.contexts()[0];
186
+
187
+ return { browser, context, debugUrl, sessionUrl };
188
+ } else {
189
+ logger({
190
+ category: "init",
191
+ message: "launching local browser",
192
+ level: 0,
193
+ auxiliary: {
194
+ headless: {
195
+ value: headless.toString(),
196
+ type: "boolean",
197
+ },
198
+ },
199
+ });
200
+
201
+ const tmpDirPath = path.join(os.tmpdir(), "stagehand");
202
+ if (!fs.existsSync(tmpDirPath)) {
203
+ fs.mkdirSync(tmpDirPath, { recursive: true });
204
+ }
205
+
206
+ const tmpDir = fs.mkdtempSync(path.join(tmpDirPath, "ctx_"));
207
+ fs.mkdirSync(path.join(tmpDir, "userdir/Default"), { recursive: true });
208
+
209
+ const defaultPreferences = {
210
+ plugins: {
211
+ always_open_pdf_externally: true,
212
+ },
213
+ };
214
+
215
+ fs.writeFileSync(
216
+ path.join(tmpDir, "userdir/Default/Preferences"),
217
+ JSON.stringify(defaultPreferences),
218
+ );
219
+
220
+ const downloadsPath = path.join(process.cwd(), "downloads");
221
+ fs.mkdirSync(downloadsPath, { recursive: true });
222
+
223
+ const context = await chromium.launchPersistentContext(
224
+ path.join(tmpDir, "userdir"),
225
+ {
226
+ acceptDownloads: true,
227
+ headless: headless,
228
+ viewport: {
229
+ width: 1250,
230
+ height: 800,
231
+ },
232
+ locale: "en-US",
233
+ timezoneId: "America/New_York",
234
+ deviceScaleFactor: 1,
235
+ args: [
236
+ "--enable-webgl",
237
+ "--use-gl=swiftshader",
238
+ "--enable-accelerated-2d-canvas",
239
+ "--disable-blink-features=AutomationControlled",
240
+ "--disable-web-security",
241
+ ],
242
+ bypassCSP: true,
243
+ },
244
+ );
245
+
246
+ logger({
247
+ category: "init",
248
+ message: "local browser started successfully.",
249
+ });
250
+
251
+ await applyStealthScripts(context);
252
+
253
+ return { context, contextPath: tmpDir };
254
+ }
255
+ }
256
+
257
+ async function applyStealthScripts(context: BrowserContext) {
258
+ await context.addInitScript(() => {
259
+ // Override the navigator.webdriver property
260
+ Object.defineProperty(navigator, "webdriver", {
261
+ get: () => undefined,
262
+ });
263
+
264
+ // Mock languages and plugins to mimic a real browser
265
+ Object.defineProperty(navigator, "languages", {
266
+ get: () => ["en-US", "en"],
267
+ });
268
+
269
+ Object.defineProperty(navigator, "plugins", {
270
+ get: () => [1, 2, 3, 4, 5],
271
+ });
272
+
273
+ // Remove Playwright-specific properties
274
+ delete (window as any).__playwright;
275
+ delete (window as any).__pw_manual;
276
+ delete (window as any).__PW_inspect;
277
+
278
+ // Redefine the headless property
279
+ Object.defineProperty(navigator, "headless", {
280
+ get: () => false,
281
+ });
282
+
283
+ // Override the permissions API
284
+ const originalQuery = window.navigator.permissions.query;
285
+ window.navigator.permissions.query = (parameters: any) =>
286
+ parameters.name === "notifications"
287
+ ? Promise.resolve({
288
+ state: Notification.permission,
289
+ } as PermissionStatus)
290
+ : originalQuery(parameters);
291
+ });
292
+ }
293
+
294
+ export class Stagehand {
295
+ private llmProvider: LLMProvider;
296
+ private llmClient: LLMClient;
297
+ public page: Page;
298
+ public context: BrowserContext;
299
+ private env: "LOCAL" | "BROWSERBASE";
300
+ private apiKey: string | undefined;
301
+ private projectId: string | undefined;
302
+ private verbose: 0 | 1 | 2;
303
+ private debugDom: boolean;
304
+ private headless: boolean;
305
+ private logger: (logLine: LogLine) => void;
306
+ private externalLogger?: (logLine: LogLine) => void;
307
+ private domSettleTimeoutMs: number;
308
+ private browserBaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams;
309
+ private enableCaching: boolean;
310
+ private variables: { [key: string]: any };
311
+ private browserbaseResumeSessionID?: string;
312
+ private contextPath?: string;
313
+
314
+ private actHandler?: StagehandActHandler;
315
+ private extractHandler?: StagehandExtractHandler;
316
+ private observeHandler?: StagehandObserveHandler;
317
+
318
+ constructor(
319
+ {
320
+ env,
321
+ apiKey,
322
+ projectId,
323
+ verbose,
324
+ debugDom,
325
+ llmProvider,
326
+ headless,
327
+ logger,
328
+ browserBaseSessionCreateParams,
329
+ domSettleTimeoutMs,
330
+ enableCaching,
331
+ browserbaseResumeSessionID,
332
+ modelName,
333
+ modelClientOptions,
334
+ }: ConstructorParams = {
335
+ env: "BROWSERBASE",
336
+ },
337
+ ) {
338
+ this.externalLogger = logger;
339
+ this.logger = this.log.bind(this);
340
+ this.enableCaching =
341
+ enableCaching ??
342
+ (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true");
343
+ this.llmProvider =
344
+ llmProvider || new LLMProvider(this.logger, this.enableCaching);
345
+ this.env = env;
346
+ this.apiKey = apiKey ?? process.env.BROWSERBASE_API_KEY;
347
+ this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID;
348
+ this.verbose = verbose ?? 0;
349
+ this.debugDom = debugDom ?? false;
350
+ this.llmClient = this.llmProvider.getClient(
351
+ modelName ?? DEFAULT_MODEL_NAME,
352
+ modelClientOptions,
353
+ );
354
+ this.domSettleTimeoutMs = domSettleTimeoutMs ?? 30_000;
355
+ this.headless = headless ?? false;
356
+ this.browserBaseSessionCreateParams = browserBaseSessionCreateParams;
357
+ this.browserbaseResumeSessionID = browserbaseResumeSessionID;
358
+ }
359
+
360
+ async init({
361
+ modelName,
362
+ modelClientOptions,
363
+ domSettleTimeoutMs,
364
+ }: InitOptions = {}): Promise<InitResult> {
365
+ const llmClient = modelName
366
+ ? this.llmProvider.getClient(modelName, modelClientOptions)
367
+ : this.llmClient;
368
+ const { context, debugUrl, sessionUrl, contextPath } = await getBrowser(
369
+ this.apiKey,
370
+ this.projectId,
371
+ this.env,
372
+ this.headless,
373
+ this.logger,
374
+ this.browserBaseSessionCreateParams,
375
+ this.browserbaseResumeSessionID,
376
+ ).catch((e) => {
377
+ console.error("Error in init:", e);
378
+ return {
379
+ context: undefined,
380
+ debugUrl: undefined,
381
+ sessionUrl: undefined,
382
+ } as BrowserResult;
383
+ });
384
+ this.contextPath = contextPath;
385
+ this.context = context;
386
+ this.page = context.pages()[0];
387
+ // Redundant but needed for users who are re-connecting to a previously-created session
388
+ await this.page.waitForLoadState("domcontentloaded");
389
+ await this._waitForSettledDom();
390
+ this.domSettleTimeoutMs = domSettleTimeoutMs ?? this.domSettleTimeoutMs;
391
+
392
+ // Overload the page.goto method
393
+ const originalGoto = this.page.goto.bind(this.page);
394
+ this.page.goto = async (url: string, options?: any) => {
395
+ const result = await originalGoto(url, options);
396
+ if (this.debugDom) {
397
+ await this.page.evaluate(() => (window.showChunks = this.debugDom));
398
+ }
399
+ await this.page.waitForLoadState("domcontentloaded");
400
+ await this._waitForSettledDom();
401
+ return result;
402
+ };
403
+
404
+ // Set the browser to headless mode if specified
405
+ if (this.headless) {
406
+ await this.page.setViewportSize({ width: 1280, height: 720 });
407
+ }
408
+
409
+ await this.context.addInitScript({
410
+ content: scriptContent,
411
+ });
412
+
413
+ this.actHandler = new StagehandActHandler({
414
+ stagehand: this,
415
+ verbose: this.verbose,
416
+ llmProvider: this.llmProvider,
417
+ enableCaching: this.enableCaching,
418
+ logger: this.logger,
419
+ waitForSettledDom: this._waitForSettledDom.bind(this),
420
+ startDomDebug: this.startDomDebug.bind(this),
421
+ cleanupDomDebug: this.cleanupDomDebug.bind(this),
422
+ llmClient,
423
+ });
424
+
425
+ this.extractHandler = new StagehandExtractHandler({
426
+ stagehand: this,
427
+ logger: this.logger,
428
+ waitForSettledDom: this._waitForSettledDom.bind(this),
429
+ startDomDebug: this.startDomDebug.bind(this),
430
+ cleanupDomDebug: this.cleanupDomDebug.bind(this),
431
+ llmProvider: this.llmProvider,
432
+ verbose: this.verbose,
433
+ llmClient,
434
+ });
435
+
436
+ this.observeHandler = new StagehandObserveHandler({
437
+ stagehand: this,
438
+ logger: this.logger,
439
+ waitForSettledDom: this._waitForSettledDom.bind(this),
440
+ startDomDebug: this.startDomDebug.bind(this),
441
+ cleanupDomDebug: this.cleanupDomDebug.bind(this),
442
+ llmProvider: this.llmProvider,
443
+ verbose: this.verbose,
444
+ llmClient,
445
+ });
446
+
447
+ this.llmClient = llmClient;
448
+ return { debugUrl, sessionUrl };
449
+ }
450
+
451
+ async initFromPage({
452
+ page,
453
+ modelName,
454
+ modelClientOptions,
455
+ }: InitFromPageOptions): Promise<InitFromPageResult> {
456
+ this.page = page;
457
+ this.context = page.context();
458
+ this.llmClient = modelName
459
+ ? this.llmProvider.getClient(modelName, modelClientOptions)
460
+ : this.llmClient;
461
+
462
+ const originalGoto = this.page.goto.bind(this.page);
463
+ this.page.goto = async (url: string, options?: any) => {
464
+ const result = await originalGoto(url, options);
465
+ if (this.debugDom) {
466
+ await this.page.evaluate(() => (window.showChunks = this.debugDom));
467
+ }
468
+ await this.page.waitForLoadState("domcontentloaded");
469
+ await this._waitForSettledDom();
470
+ return result;
471
+ };
472
+
473
+ // Set the browser to headless mode if specified
474
+ if (this.headless) {
475
+ await this.page.setViewportSize({ width: 1280, height: 720 });
476
+ }
477
+
478
+ // Add initialization scripts
479
+ await this.context.addInitScript({
480
+ content: scriptContent,
481
+ });
482
+
483
+ return { context: this.context };
484
+ }
485
+
486
+ private pending_logs_to_send_to_browserbase: LogLine[] = [];
487
+
488
+ private is_processing_browserbase_logs: boolean = false;
489
+
490
+ log(logObj: LogLine): void {
491
+ logObj.level = logObj.level || 1;
492
+
493
+ // Normal Logging
494
+ if (this.externalLogger) {
495
+ this.externalLogger(logObj);
496
+ } else {
497
+ const logMessage = logLineToString(logObj);
498
+ console.log(logMessage);
499
+ }
500
+
501
+ // Add the logs to the browserbase session
502
+ this.pending_logs_to_send_to_browserbase.push({
503
+ ...logObj,
504
+ id: randomUUID(),
505
+ });
506
+ this._run_browserbase_log_processing_cycle();
507
+ }
508
+
509
+ private async _run_browserbase_log_processing_cycle() {
510
+ if (this.is_processing_browserbase_logs) {
511
+ return;
512
+ }
513
+ this.is_processing_browserbase_logs = true;
514
+ const pending_logs = [...this.pending_logs_to_send_to_browserbase];
515
+ for (const logObj of pending_logs) {
516
+ await this._log_to_browserbase(logObj);
517
+ }
518
+ this.is_processing_browserbase_logs = false;
519
+ }
520
+
521
+ private async _log_to_browserbase(logObj: LogLine) {
522
+ logObj.level = logObj.level || 1;
523
+
524
+ if (!this.page) {
525
+ return;
526
+ }
527
+
528
+ if (this.verbose >= logObj.level) {
529
+ await this.page
530
+ .evaluate((logObj) => {
531
+ const logMessage = logLineToString(logObj);
532
+ if (
533
+ logObj.message.toLowerCase().includes("trace") ||
534
+ logObj.message.toLowerCase().includes("error:")
535
+ ) {
536
+ console.error(logMessage);
537
+ } else {
538
+ console.log(logMessage);
539
+ }
540
+ }, logObj)
541
+ .then(() => {
542
+ this.pending_logs_to_send_to_browserbase =
543
+ this.pending_logs_to_send_to_browserbase.filter(
544
+ (log) => log.id !== logObj.id,
545
+ );
546
+ })
547
+ .catch((e) => {
548
+ // NAVIDTODO: Rerun the log call on the new page
549
+ // This is expected to happen when the user is changing pages
550
+ // console.error("Logging Error:", e);
551
+ });
552
+ }
553
+ }
554
+
555
+ private async _waitForSettledDom(timeoutMs?: number) {
556
+ try {
557
+ const timeout = timeoutMs ?? this.domSettleTimeoutMs;
558
+ let timeoutHandle: NodeJS.Timeout;
559
+
560
+ const timeoutPromise = new Promise<void>((resolve, reject) => {
561
+ timeoutHandle = setTimeout(() => {
562
+ this.log({
563
+ category: "dom",
564
+ message: "DOM settle timeout exceeded, continuing anyway",
565
+ level: 1,
566
+ auxiliary: {
567
+ timeout_ms: {
568
+ value: timeout.toString(),
569
+ type: "integer",
570
+ },
571
+ },
572
+ });
573
+ resolve();
574
+ }, timeout);
575
+ });
576
+
577
+ try {
578
+ await Promise.race([
579
+ this.page.evaluate(() => {
580
+ return new Promise<void>((resolve) => {
581
+ if (typeof window.waitForDomSettle === "function") {
582
+ window.waitForDomSettle().then(resolve);
583
+ } else {
584
+ console.warn(
585
+ "waitForDomSettle is not defined, considering DOM as settled",
586
+ );
587
+ resolve();
588
+ }
589
+ });
590
+ }),
591
+ this.page.waitForLoadState("domcontentloaded"),
592
+ this.page.waitForSelector("body"),
593
+ timeoutPromise,
594
+ ]);
595
+ } finally {
596
+ clearTimeout(timeoutHandle!);
597
+ }
598
+ } catch (e) {
599
+ this.log({
600
+ category: "dom",
601
+ message: "Error in waitForSettledDom",
602
+ level: 1,
603
+ auxiliary: {
604
+ error: {
605
+ value: e.message,
606
+ type: "string",
607
+ },
608
+ trace: {
609
+ value: e.stack,
610
+ type: "string",
611
+ },
612
+ },
613
+ });
614
+ }
615
+ }
616
+
617
+ private async startDomDebug() {
618
+ try {
619
+ await this.page
620
+ .evaluate(() => {
621
+ if (typeof window.debugDom === "function") {
622
+ window.debugDom();
623
+ } else {
624
+ this.log({
625
+ category: "dom",
626
+ message: "debugDom is not defined",
627
+ level: 1,
628
+ });
629
+ }
630
+ })
631
+ .catch(() => {});
632
+ } catch (e) {
633
+ this.log({
634
+ category: "dom",
635
+ message: "Error in startDomDebug",
636
+ level: 1,
637
+ auxiliary: {
638
+ error: {
639
+ value: e.message,
640
+ type: "string",
641
+ },
642
+ trace: {
643
+ value: e.stack,
644
+ type: "string",
645
+ },
646
+ },
647
+ });
648
+ }
649
+ }
650
+
651
+ private async cleanupDomDebug() {
652
+ if (this.debugDom) {
653
+ await this.page.evaluate(() => window.cleanupDebug()).catch(() => {});
654
+ }
655
+ }
656
+
657
+ async act({
658
+ action,
659
+ modelName,
660
+ modelClientOptions,
661
+ useVision = "fallback",
662
+ variables = {},
663
+ domSettleTimeoutMs,
664
+ }: ActOptions): Promise<ActResult> {
665
+ if (!this.actHandler) {
666
+ throw new Error("Act handler not initialized");
667
+ }
668
+
669
+ useVision = useVision ?? "fallback";
670
+ const requestId = Math.random().toString(36).substring(2);
671
+ const llmClient: LLMClient = modelName
672
+ ? this.llmProvider.getClient(modelName, modelClientOptions)
673
+ : this.llmClient;
674
+
675
+ this.log({
676
+ category: "act",
677
+ message: "running act",
678
+ level: 1,
679
+ auxiliary: {
680
+ action: {
681
+ value: action,
682
+ type: "string",
683
+ },
684
+ requestId: {
685
+ value: requestId,
686
+ type: "string",
687
+ },
688
+ modelName: {
689
+ value: llmClient.modelName,
690
+ type: "string",
691
+ },
692
+ },
693
+ });
694
+
695
+ if (variables) {
696
+ this.variables = { ...this.variables, ...variables };
697
+ }
698
+
699
+ return this.actHandler
700
+ .act({
701
+ action,
702
+ llmClient,
703
+ chunksSeen: [],
704
+ useVision,
705
+ verifierUseVision: useVision !== false,
706
+ requestId,
707
+ variables,
708
+ previousSelectors: [],
709
+ skipActionCacheForThisStep: false,
710
+ domSettleTimeoutMs,
711
+ })
712
+ .catch((e) => {
713
+ this.log({
714
+ category: "act",
715
+ message: "error acting",
716
+ level: 1,
717
+ auxiliary: {
718
+ error: {
719
+ value: e.message,
720
+ type: "string",
721
+ },
722
+ trace: {
723
+ value: e.stack,
724
+ type: "string",
725
+ },
726
+ },
727
+ });
728
+
729
+ return {
730
+ success: false,
731
+ message: `Internal error: Error acting: ${e.message}`,
732
+ action: action,
733
+ };
734
+ });
735
+ }
736
+
737
+ async extract<T extends z.AnyZodObject>({
738
+ instruction,
739
+ schema,
740
+ modelName,
741
+ modelClientOptions,
742
+ domSettleTimeoutMs,
743
+ }: ExtractOptions<T>): Promise<ExtractResult<T>> {
744
+ if (!this.extractHandler) {
745
+ throw new Error("Extract handler not initialized");
746
+ }
747
+
748
+ const requestId = Math.random().toString(36).substring(2);
749
+ const llmClient = modelName
750
+ ? this.llmProvider.getClient(modelName, modelClientOptions)
751
+ : this.llmClient;
752
+
753
+ this.logger({
754
+ category: "extract",
755
+ message: "running extract",
756
+ level: 1,
757
+ auxiliary: {
758
+ instruction: {
759
+ value: instruction,
760
+ type: "string",
761
+ },
762
+ requestId: {
763
+ value: requestId,
764
+ type: "string",
765
+ },
766
+ modelName: {
767
+ value: llmClient.modelName,
768
+ type: "string",
769
+ },
770
+ },
771
+ });
772
+
773
+ return this.extractHandler
774
+ .extract({
775
+ instruction,
776
+ schema,
777
+ llmClient,
778
+ requestId,
779
+ domSettleTimeoutMs,
780
+ })
781
+ .catch((e) => {
782
+ this.logger({
783
+ category: "extract",
784
+ message: "error extracting",
785
+ level: 1,
786
+ auxiliary: {
787
+ error: {
788
+ value: e.message,
789
+ type: "string",
790
+ },
791
+ trace: {
792
+ value: e.stack,
793
+ type: "string",
794
+ },
795
+ },
796
+ });
797
+
798
+ if (this.enableCaching) {
799
+ this.llmProvider.cleanRequestCache(requestId);
800
+ }
801
+
802
+ throw e;
803
+ });
804
+ }
805
+
806
+ async observe(options?: ObserveOptions): Promise<ObserveResult[]> {
807
+ if (!this.observeHandler) {
808
+ throw new Error("Observe handler not initialized");
809
+ }
810
+
811
+ const requestId = Math.random().toString(36).substring(2);
812
+ const llmClient = options?.modelName
813
+ ? this.llmProvider.getClient(
814
+ options.modelName,
815
+ options.modelClientOptions,
816
+ )
817
+ : this.llmClient;
818
+
819
+ this.logger({
820
+ category: "observe",
821
+ message: "running observe",
822
+ level: 1,
823
+ auxiliary: {
824
+ instruction: {
825
+ value: options?.instruction,
826
+ type: "string",
827
+ },
828
+ requestId: {
829
+ value: requestId,
830
+ type: "string",
831
+ },
832
+ modelName: {
833
+ value: llmClient.modelName,
834
+ type: "string",
835
+ },
836
+ },
837
+ });
838
+
839
+ return this.observeHandler
840
+ .observe({
841
+ instruction:
842
+ options?.instruction ??
843
+ "Find actions that can be performed on this page.",
844
+ llmClient,
845
+ useVision: options?.useVision ?? false,
846
+ fullPage: false,
847
+ requestId,
848
+ domSettleTimeoutMs: options?.domSettleTimeoutMs,
849
+ })
850
+ .catch((e) => {
851
+ this.logger({
852
+ category: "observe",
853
+ message: "error observing",
854
+ level: 1,
855
+ auxiliary: {
856
+ error: {
857
+ value: e.message,
858
+ type: "string",
859
+ },
860
+ trace: {
861
+ value: e.stack,
862
+ type: "string",
863
+ },
864
+ requestId: {
865
+ value: requestId,
866
+ type: "string",
867
+ },
868
+ instruction: {
869
+ value: options?.instruction,
870
+ type: "string",
871
+ },
872
+ },
873
+ });
874
+
875
+ if (this.enableCaching) {
876
+ this.llmProvider.cleanRequestCache(requestId);
877
+ }
878
+
879
+ throw e;
880
+ });
881
+ }
882
+
883
+ async close(): Promise<void> {
884
+ await this.context.close();
885
+
886
+ if (this.contextPath) {
887
+ try {
888
+ fs.rmSync(this.contextPath, { recursive: true, force: true });
889
+ } catch (e) {
890
+ console.error("Error deleting context directory:", e);
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ export * from "../types/browser";
897
+ export * from "../types/log";
898
+ export * from "../types/model";
899
+ export * from "../types/playwright";
900
+ export * from "../types/stagehand";