@mastra/stagehand 0.1.0 → 0.2.0-alpha.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @mastra/stagehand
2
2
 
3
+ ## 0.2.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added automatic cleanup on browser close: patches `exit_type` to prevent restore dialogs, kills orphaned Chrome child processes, and uses CDP events for reliable disconnect detection in both shared and thread scope. ([#15194](https://github.com/mastra-ai/mastra/pull/15194))
8
+
9
+ ### Patch Changes
10
+
11
+ - dependencies updates: ([#15209](https://github.com/mastra-ai/mastra/pull/15209))
12
+ - Updated dependency [`@browserbasehq/stagehand@^3.2.1` ↗︎](https://www.npmjs.com/package/@browserbasehq/stagehand/v/3.2.1) (from `^3.2.0`, in `dependencies`)
13
+ - Updated dependencies [[`cbdf3e1`](https://github.com/mastra-ai/mastra/commit/cbdf3e12b3d0c30a6e5347be658e2009648c130a), [`8fe46d3`](https://github.com/mastra-ai/mastra/commit/8fe46d354027f3f0f0846e64219772348de106dd), [`18c67db`](https://github.com/mastra-ai/mastra/commit/18c67dbb9c9ebc26f26f65f7d3ff836e5691ef46), [`8dcc77e`](https://github.com/mastra-ai/mastra/commit/8dcc77e78a5340f5848f74b9e9f1b3da3513c1f5), [`aa67fc5`](https://github.com/mastra-ai/mastra/commit/aa67fc59ee8a5eeff1f23eb05970b8d7a536c8ff), [`fa8140b`](https://github.com/mastra-ai/mastra/commit/fa8140bcd4251d2e3ac85fdc5547dfc4f372b5be), [`190f452`](https://github.com/mastra-ai/mastra/commit/190f45258b0640e2adfc8219fa3258cdc5b8f071), [`7e7bf60`](https://github.com/mastra-ai/mastra/commit/7e7bf606886bf374a6f9d4ca9b09dd83d0533372), [`184907d`](https://github.com/mastra-ai/mastra/commit/184907d775d8609c03c26e78ccaf37315f3aa287), [`0c4cd13`](https://github.com/mastra-ai/mastra/commit/0c4cd131931c04ac5405373c932a242dbe88edd6), [`b16a753`](https://github.com/mastra-ai/mastra/commit/b16a753d5748440248d7df82e29bb987a9c8386c)]:
14
+ - @mastra/core@1.25.0-alpha.3
15
+
3
16
  ## 0.1.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -11,12 +11,12 @@ npm install @mastra/stagehand
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { Agent } from '@mastra/core';
14
+ import { Agent } from '@mastra/core/agent';
15
15
  import { StagehandBrowser } from '@mastra/stagehand';
16
16
 
17
17
  // Create a Stagehand browser
18
18
  const browser = new StagehandBrowser({
19
- model: 'openai/gpt-4o',
19
+ model: 'openai/gpt-5.4',
20
20
  headless: true,
21
21
  });
22
22
 
@@ -24,7 +24,7 @@ const browser = new StagehandBrowser({
24
24
  const agent = new Agent({
25
25
  name: 'web-agent',
26
26
  instructions: 'You are a helpful web assistant.',
27
- model: { provider: 'openai', modelId: 'gpt-4o' },
27
+ model: 'openai/gpt-5.4',
28
28
  browser,
29
29
  });
30
30
 
@@ -39,17 +39,20 @@ const browser = new StagehandBrowser({
39
39
  // Environment: 'LOCAL' or 'BROWSERBASE'
40
40
  env: 'LOCAL',
41
41
 
42
- // Model for AI operations (default: 'openai/gpt-4o')
43
- model: 'openai/gpt-4o',
42
+ // Model for AI operations (default: 'openai/gpt-5.4')
43
+ model: 'openai/gpt-5.4',
44
44
  // Or with custom config:
45
45
  model: {
46
- modelName: 'openai/gpt-4o',
46
+ modelName: 'gpt-5.4',
47
47
  apiKey: process.env.OPENAI_API_KEY,
48
48
  },
49
49
 
50
50
  // Run headless (default: true)
51
51
  headless: true,
52
52
 
53
+ // Viewport dimensions
54
+ viewport: { width: 1280, height: 720 },
55
+
53
56
  // CDP URL for connecting to existing browser
54
57
  cdpUrl: 'ws://localhost:9222',
55
58
 
@@ -57,8 +60,17 @@ const browser = new StagehandBrowser({
57
60
  apiKey: process.env.BROWSERBASE_API_KEY,
58
61
  projectId: process.env.BROWSERBASE_PROJECT_ID,
59
62
 
63
+ // Enable self-healing selectors (default: true)
64
+ selfHeal: true,
65
+
66
+ // DOM settle timeout in ms (default: 5000)
67
+ domSettleTimeout: 5000,
68
+
60
69
  // Logging verbosity (0: silent, 1: errors, 2: verbose)
61
70
  verbose: 1,
71
+
72
+ // Custom system prompt for AI operations
73
+ systemPrompt: 'Focus on finding interactive elements',
62
74
  });
63
75
  ```
64
76
 
@@ -68,25 +80,30 @@ StagehandBrowser exposes 6 AI-powered tools:
68
80
 
69
81
  ### Core AI Tools
70
82
 
71
- - **stagehand_act** - Perform actions using natural language
83
+ - **stagehand_act** - Perform actions using natural language instructions
72
84
  - **stagehand_extract** - Extract structured data from pages
73
- - **stagehand_observe** - Discover actionable elements
85
+ - **stagehand_observe** - Discover actionable elements on the page
74
86
 
75
87
  ### Navigation & State
76
88
 
77
89
  - **stagehand_navigate** - Navigate to a URL
78
- - **stagehand_screenshot** - Take screenshots
90
+ - **stagehand_tabs** - List, switch, open, or close browser tabs
79
91
  - **stagehand_close** - Close the browser
80
92
 
81
93
  ## Comparison with AgentBrowser
82
94
 
83
- | Feature | AgentBrowser | StagehandBrowser |
84
- | ----------- | ----------------------------- | ---------------------------- |
85
- | Approach | Deterministic refs ([ref=e1]) | Natural language |
86
- | Token cost | Low | Higher (LLM calls) |
87
- | Speed | Fast | Slower |
88
- | Reliability | High (exact refs) | Variable (AI interpretation) |
89
- | Best for | Structured workflows | Unknown/dynamic pages |
95
+ | Feature | AgentBrowser | StagehandBrowser |
96
+ | ----------- | ------------------------ | ---------------------------- |
97
+ | Approach | Deterministic refs (@e1) | Natural language |
98
+ | Token cost | Low | Higher (LLM calls) |
99
+ | Speed | Fast | Slower |
100
+ | Reliability | High (exact refs) | Variable (AI interpretation) |
101
+ | Best for | Structured workflows | Unknown/dynamic pages |
102
+
103
+ ## Documentation
104
+
105
+ - [Stagehand guide](https://mastra.ai/docs/browser/stagehand) - Usage guide
106
+ - [StagehandBrowser reference](https://mastra.ai/reference/browser/stagehand-browser) - API reference
90
107
 
91
108
  ## License
92
109
 
package/dist/index.cjs CHANGED
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var fs = require('fs');
3
4
  var stagehand = require('@browserbasehq/stagehand');
4
5
  var browser = require('@mastra/core/browser');
5
6
  var tools = require('@mastra/core/tools');
6
7
  var zod = require('zod');
8
+ var path = require('path');
7
9
 
8
10
  // src/stagehand-browser.ts
9
11
  var StagehandThreadManager = class extends browser.ThreadManager {
@@ -280,6 +282,30 @@ function createStagehandTools(browser) {
280
282
  [STAGEHAND_TOOLS.CLOSE]: createCloseTool(browser)
281
283
  };
282
284
  }
285
+ function patchProfileExitType(profilePath, logger) {
286
+ if (!profilePath) return;
287
+ const prefsPath = path.join(profilePath, "Default", "Preferences");
288
+ try {
289
+ if (!fs.existsSync(prefsPath)) return;
290
+ const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
291
+ if (prefs?.profile?.exit_type === "Normal") return;
292
+ prefs.profile = prefs.profile || {};
293
+ prefs.profile.exit_type = "Normal";
294
+ fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2), "utf-8");
295
+ logger?.debug?.(`Patched exit_type to Normal in ${prefsPath}`);
296
+ } catch {
297
+ }
298
+ }
299
+ function getStagehandChromePid(stagehand) {
300
+ try {
301
+ const state = stagehand.state;
302
+ if (state?.kind !== "LOCAL" || !state.chrome) return void 0;
303
+ const pid = state.chrome.process?.pid ?? state.chrome.pid;
304
+ return typeof pid === "number" && pid > 0 ? pid : void 0;
305
+ } catch {
306
+ return void 0;
307
+ }
308
+ }
283
309
 
284
310
  // src/stagehand-browser.ts
285
311
  var StagehandBrowser = class extends browser.MastraBrowser {
@@ -303,7 +329,7 @@ var StagehandBrowser = class extends browser.MastraBrowser {
303
329
  },
304
330
  // When a new browser is created for a thread, set up close listener
305
331
  onBrowserCreated: (stagehand, threadId) => {
306
- this.setupCloseListenerForThread(stagehand, threadId);
332
+ this.setupCloseListener(stagehand, () => this.handleThreadBrowserDisconnected(threadId), threadId);
307
333
  }
308
334
  });
309
335
  }
@@ -346,18 +372,27 @@ var StagehandBrowser = class extends browser.MastraBrowser {
346
372
  stagehandOptions.projectId = config.projectId;
347
373
  }
348
374
  }
375
+ if (config.profile && !fs.existsSync(config.profile)) {
376
+ fs.mkdirSync(config.profile, { recursive: true });
377
+ }
349
378
  if (config.cdpUrl && config.env !== "BROWSERBASE") {
350
379
  const resolvedUrl = await this.resolveCdpUrl(config.cdpUrl);
351
380
  const wsUrl = await this.resolveWebSocketUrl(resolvedUrl);
352
381
  stagehandOptions.localBrowserLaunchOptions = {
353
382
  cdpUrl: wsUrl,
354
383
  headless: config.headless,
355
- viewport: config.viewport
384
+ viewport: config.viewport,
385
+ userDataDir: config.profile,
386
+ executablePath: config.executablePath,
387
+ preserveUserDataDir: config.preserveUserDataDir
356
388
  };
357
389
  } else if (config.env !== "BROWSERBASE") {
358
390
  stagehandOptions.localBrowserLaunchOptions = {
359
391
  headless: config.headless,
360
- viewport: config.viewport
392
+ viewport: config.viewport,
393
+ userDataDir: config.profile,
394
+ executablePath: config.executablePath,
395
+ preserveUserDataDir: config.preserveUserDataDir
361
396
  };
362
397
  }
363
398
  return stagehandOptions;
@@ -380,81 +415,54 @@ var StagehandBrowser = class extends browser.MastraBrowser {
380
415
  }
381
416
  this.sharedManager = await this.createStagehandInstance();
382
417
  this.threadManager.setSharedManager(this.sharedManager);
383
- this.setupCloseListenerForSharedScope(this.sharedManager);
418
+ this.setupCloseListener(this.sharedManager, () => this.handleBrowserDisconnected());
384
419
  }
385
420
  /**
386
421
  * Set up close event listener for a shared Stagehand instance.
387
422
  * Listens to both context and page close events for robust detection.
388
423
  */
389
- setupCloseListenerForSharedScope(stagehand) {
390
- let disconnectHandled = false;
391
- const handleDisconnect = () => {
392
- if (disconnectHandled) return;
393
- disconnectHandled = true;
394
- this.handleBrowserDisconnected();
395
- };
396
- try {
397
- const context = stagehand.context;
398
- if (!context) return;
399
- const contextWithEvents = context;
400
- if (contextWithEvents?.on) {
401
- contextWithEvents.on("close", handleDisconnect);
402
- }
403
- const pages = context.pages?.() ?? [];
404
- for (const page of pages) {
405
- const pageWithEvents = page;
406
- if (pageWithEvents?.on) {
407
- pageWithEvents.on("close", () => {
408
- const remainingPages = context.pages?.() ?? [];
409
- if (remainingPages.length === 0) {
410
- handleDisconnect();
411
- }
412
- });
413
- }
414
- }
415
- } catch {
416
- }
417
- }
418
424
  /**
419
- * Set up close event listener for a thread's Stagehand instance.
420
- * Uses CDP Target.targetDestroyed events to detect when all pages are gone.
425
+ * Set up a CDP-based close listener for a Stagehand instance.
426
+ *
427
+ * Tracks page targets via CDP `Target.targetCreated` / `Target.targetDestroyed`.
428
+ * When all page targets are gone the `onDisconnect` callback fires. This is more
429
+ * reliable than Playwright's `context.close` / `page.close` events which don't
430
+ * fire when Chrome is killed externally (SIGTERM/SIGKILL).
421
431
  */
422
- setupCloseListenerForThread(stagehand, threadId) {
432
+ setupCloseListener(stagehand, onDisconnect, threadId) {
433
+ const chromePid = getStagehandChromePid(stagehand);
434
+ if (chromePid != null) {
435
+ if (threadId) {
436
+ this.threadBrowserPids.set(threadId, chromePid);
437
+ } else {
438
+ this.sharedBrowserPid = chromePid;
439
+ }
440
+ }
423
441
  let disconnectHandled = false;
424
442
  const handleDisconnect = () => {
425
443
  if (disconnectHandled) return;
426
444
  disconnectHandled = true;
427
- this.handleThreadBrowserDisconnected(threadId);
445
+ onDisconnect();
428
446
  };
429
447
  try {
430
448
  const stagehandAny = stagehand;
431
449
  const conn = stagehandAny.ctx?.conn;
432
- if (!conn?.on) {
433
- return;
434
- }
450
+ if (!conn?.on) return;
435
451
  const pageTargets = /* @__PURE__ */ new Set();
436
452
  const context = stagehand.context;
437
453
  if (context) {
438
- const pages = context.pages?.() ?? [];
439
- for (const page of pages) {
440
- const pageAny = page;
441
- const targetId = pageAny._targetId ?? pageAny.targetId;
442
- if (targetId) {
443
- pageTargets.add(targetId);
444
- }
454
+ for (const page of context.pages?.() ?? []) {
455
+ const targetId = page._targetId ?? page.targetId;
456
+ if (targetId) pageTargets.add(targetId);
445
457
  }
446
458
  }
447
459
  conn.on("Target.targetCreated", (params) => {
448
- if (params.targetInfo.type === "page") {
449
- pageTargets.add(params.targetInfo.targetId);
450
- }
460
+ if (params.targetInfo.type === "page") pageTargets.add(params.targetInfo.targetId);
451
461
  });
452
462
  conn.on("Target.targetDestroyed", (params) => {
453
463
  if (pageTargets.has(params.targetId)) {
454
464
  pageTargets.delete(params.targetId);
455
- if (pageTargets.size === 0) {
456
- handleDisconnect();
457
- }
465
+ if (pageTargets.size === 0) handleDisconnect();
458
466
  }
459
467
  });
460
468
  } catch {
@@ -467,6 +475,23 @@ var StagehandBrowser = class extends browser.MastraBrowser {
467
475
  this.sharedManager = null;
468
476
  }
469
477
  this.setCurrentThread(void 0);
478
+ this.patchExitType();
479
+ }
480
+ handleBrowserDisconnected() {
481
+ super.handleBrowserDisconnected();
482
+ this.patchExitType();
483
+ }
484
+ handleThreadBrowserDisconnected(threadId) {
485
+ super.handleThreadBrowserDisconnected(threadId);
486
+ this.patchExitType();
487
+ }
488
+ async closeThreadSession(threadId) {
489
+ await super.closeThreadSession(threadId);
490
+ this.patchExitType();
491
+ }
492
+ patchExitType() {
493
+ if (!this.config.profile) return;
494
+ patchProfileExitType(this.config.profile, this.logger);
470
495
  }
471
496
  /**
472
497
  * Check if the browser is still alive by verifying the context and pages exist.
@@ -1196,6 +1221,7 @@ exports.actInputSchema = actInputSchema;
1196
1221
  exports.closeInputSchema = closeInputSchema;
1197
1222
  exports.createStagehandTools = createStagehandTools;
1198
1223
  exports.extractInputSchema = extractInputSchema;
1224
+ exports.getStagehandChromePid = getStagehandChromePid;
1199
1225
  exports.navigateInputSchema = navigateInputSchema;
1200
1226
  exports.observeInputSchema = observeInputSchema;
1201
1227
  exports.stagehandSchemas = stagehandSchemas;