@mastra/stagehand 0.3.1 → 0.3.2-alpha.1

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/dist/index.cjs CHANGED
@@ -1,1312 +1,1224 @@
1
- 'use strict';
2
-
3
- var fs = require('fs');
4
- var stagehand = require('@browserbasehq/stagehand');
5
- var browser = require('@mastra/core/browser');
6
- var tools = require('@mastra/core/tools');
7
- var zod = require('zod');
8
- var path = require('path');
9
-
10
- // src/stagehand-browser.ts
11
- var StagehandThreadManager = class extends browser.ThreadManager {
12
- sessions = /* @__PURE__ */ new Map();
13
- createStagehand;
14
- onBrowserCreated;
15
- constructor(config) {
16
- super(config);
17
- this.createStagehand = config.createStagehand;
18
- this.onBrowserCreated = config.onBrowserCreated;
19
- }
20
- /**
21
- * Set the factory function for creating new Stagehand instances.
22
- * Required for 'thread' scope mode.
23
- */
24
- setCreateStagehand(factory) {
25
- this.createStagehand = factory;
26
- }
27
- /**
28
- * Get the page for a specific thread, creating session if needed.
29
- */
30
- async getPageForThread(threadId) {
31
- const stagehand = await this.getManagerForThread(threadId);
32
- return stagehand?.context?.activePage() ?? null;
33
- }
34
- /**
35
- * Create a new session for a thread.
36
- */
37
- async createSession(threadId) {
38
- const savedState = this.getSavedBrowserState(threadId);
39
- const session = {
40
- threadId,
41
- createdAt: Date.now(),
42
- browserState: savedState
43
- };
44
- if (this.scope === "thread") {
45
- if (!this.createStagehand) {
46
- throw new Error("createStagehand factory not set - required for thread scope");
47
- }
48
- this.logger?.debug?.(`Creating dedicated Stagehand instance for thread ${threadId}`);
49
- const stagehand = await this.createStagehand();
50
- session.stagehand = stagehand;
51
- this.threadManagers.set(threadId, stagehand);
52
- if (savedState && savedState.tabs.length > 0) {
53
- this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
54
- await this.restoreBrowserState(stagehand, savedState);
55
- }
56
- this.onBrowserCreated?.(stagehand, threadId);
57
- }
58
- return session;
59
- }
60
- /**
61
- * Restore browser state (multiple tabs) to a Stagehand instance.
62
- */
63
- async restoreBrowserState(stagehand, state) {
64
- try {
65
- const context = stagehand.context;
66
- if (!context) return;
67
- const firstTab = state.tabs[0];
68
- if (firstTab?.url) {
69
- const page = context.activePage();
70
- if (page) {
71
- await page.goto(firstTab.url, { waitUntil: "domcontentloaded" });
72
- }
73
- }
74
- for (let i = 1; i < state.tabs.length; i++) {
75
- const tab = state.tabs[i];
76
- if (tab?.url) {
77
- await context.newPage(tab.url);
78
- }
79
- }
80
- const pages = context.pages();
81
- const targetPage = pages[state.activeTabIndex];
82
- if (targetPage && targetPage !== context.activePage()) {
83
- context.setActivePage(targetPage);
84
- }
85
- } catch (error) {
86
- this.logger?.warn?.(`Failed to restore browser state: ${error}`);
87
- }
88
- }
89
- /**
90
- * Get the manager (Stagehand instance) for a specific session.
91
- */
92
- getManagerForSession(session) {
93
- if (this.scope === "thread" && session.stagehand) {
94
- return session.stagehand;
95
- }
96
- return this.getSharedManager();
97
- }
98
- /**
99
- * Destroy a session and clean up resources.
100
- */
101
- async doDestroySession(session) {
102
- if (this.scope === "thread" && session.stagehand) {
103
- try {
104
- await session.stagehand.close();
105
- this.logger?.debug?.(`Closed Stagehand instance for thread ${session.threadId}`);
106
- } catch (error) {
107
- this.logger?.warn?.(`Failed to close Stagehand for thread ${session.threadId}: ${error}`);
108
- }
109
- }
110
- }
111
- /**
112
- * Destroy all sessions (called during browser close).
113
- * doDestroySession handles closing individual Stagehand instances.
114
- */
115
- async destroyAllSessions() {
116
- await super.destroyAllSessions();
117
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let fs = require("fs");
3
+ let _browserbasehq_stagehand = require("@browserbasehq/stagehand");
4
+ let _mastra_core_browser = require("@mastra/core/browser");
5
+ let _mastra_core_tools = require("@mastra/core/tools");
6
+ let zod = require("zod");
7
+ let path = require("path");
8
+ //#region src/thread-manager.ts
9
+ /**
10
+ * Thread manager for StagehandBrowser.
11
+ *
12
+ * Supports two scope modes:
13
+ * - 'shared': All threads share the shared Stagehand instance
14
+ * - 'thread': Each thread gets a dedicated Stagehand instance
15
+ */
16
+ var StagehandThreadManager = class extends _mastra_core_browser.ThreadManager {
17
+ sessions = /* @__PURE__ */ new Map();
18
+ createStagehand;
19
+ onBrowserCreated;
20
+ constructor(config) {
21
+ super(config);
22
+ this.createStagehand = config.createStagehand;
23
+ this.onBrowserCreated = config.onBrowserCreated;
24
+ }
25
+ /**
26
+ * Set the factory function for creating new Stagehand instances.
27
+ * Required for 'thread' scope mode.
28
+ */
29
+ setCreateStagehand(factory) {
30
+ this.createStagehand = factory;
31
+ }
32
+ /**
33
+ * Get the page for a specific thread, creating session if needed.
34
+ */
35
+ async getPageForThread(threadId) {
36
+ return (await this.getManagerForThread(threadId))?.context?.activePage() ?? null;
37
+ }
38
+ /**
39
+ * Create a new session for a thread.
40
+ */
41
+ async createSession(threadId) {
42
+ const savedState = this.getSavedBrowserState(threadId);
43
+ const session = {
44
+ threadId,
45
+ createdAt: Date.now(),
46
+ browserState: savedState
47
+ };
48
+ if (this.scope === "thread") {
49
+ if (!this.createStagehand) throw new Error("createStagehand factory not set - required for thread scope");
50
+ this.logger?.debug?.(`Creating dedicated Stagehand instance for thread ${threadId}`);
51
+ const stagehand = await this.createStagehand();
52
+ session.stagehand = stagehand;
53
+ this.threadManagers.set(threadId, stagehand);
54
+ if (savedState && savedState.tabs.length > 0) {
55
+ this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
56
+ await this.restoreBrowserState(stagehand, savedState);
57
+ }
58
+ this.onBrowserCreated?.(stagehand, threadId);
59
+ }
60
+ return session;
61
+ }
62
+ /**
63
+ * Restore browser state (multiple tabs) to a Stagehand instance.
64
+ */
65
+ async restoreBrowserState(stagehand, state) {
66
+ try {
67
+ const context = stagehand.context;
68
+ if (!context) return;
69
+ const firstTab = state.tabs[0];
70
+ if (firstTab?.url) {
71
+ const page = context.activePage();
72
+ if (page) await page.goto(firstTab.url, { waitUntil: "domcontentloaded" });
73
+ }
74
+ for (let i = 1; i < state.tabs.length; i++) {
75
+ const tab = state.tabs[i];
76
+ if (tab?.url) await context.newPage(tab.url);
77
+ }
78
+ const targetPage = context.pages()[state.activeTabIndex];
79
+ if (targetPage && targetPage !== context.activePage()) context.setActivePage(targetPage);
80
+ } catch (error) {
81
+ this.logger?.warn?.(`Failed to restore browser state: ${error}`);
82
+ }
83
+ }
84
+ /**
85
+ * Get the manager (Stagehand instance) for a specific session.
86
+ */
87
+ getManagerForSession(session) {
88
+ if (this.scope === "thread" && session.stagehand) return session.stagehand;
89
+ return this.getSharedManager();
90
+ }
91
+ /**
92
+ * Destroy a session and clean up resources.
93
+ */
94
+ async doDestroySession(session) {
95
+ if (this.scope === "thread" && session.stagehand) try {
96
+ await session.stagehand.close();
97
+ this.logger?.debug?.(`Closed Stagehand instance for thread ${session.threadId}`);
98
+ } catch (error) {
99
+ this.logger?.warn?.(`Failed to close Stagehand for thread ${session.threadId}: ${error}`);
100
+ }
101
+ }
102
+ /**
103
+ * Destroy all sessions (called during browser close).
104
+ * doDestroySession handles closing individual Stagehand instances.
105
+ */
106
+ async destroyAllSessions() {
107
+ await super.destroyAllSessions();
108
+ }
118
109
  };
119
- var actInputSchema = zod.z.object({
120
- instruction: zod.z.string().describe('Natural language instruction for the action (e.g., "click the login button")'),
121
- variables: zod.z.record(zod.z.string(), zod.z.string()).optional().describe("Variables to substitute in the instruction using %variableName% syntax"),
122
- useVision: zod.z.boolean().optional().describe("Whether to use vision capabilities (default: true)"),
123
- timeout: zod.z.number().optional().describe("Timeout in milliseconds")
110
+ //#endregion
111
+ //#region src/schemas.ts
112
+ /**
113
+ * Stagehand Tool Schemas
114
+ *
115
+ * AI-powered browser tools using natural language instructions.
116
+ * These are fundamentally different from the deterministic AgentBrowser tools.
117
+ */
118
+ /**
119
+ * stagehand_act - Perform an action using natural language
120
+ */
121
+ const actInputSchema = zod.z.object({
122
+ instruction: zod.z.string().describe("Natural language instruction for the action (e.g., \"click the login button\")"),
123
+ variables: zod.z.record(zod.z.string(), zod.z.string()).optional().describe("Variables to substitute in the instruction using %variableName% syntax"),
124
+ useVision: zod.z.boolean().optional().describe("Whether to use vision capabilities (default: true)"),
125
+ timeout: zod.z.number().optional().describe("Timeout in milliseconds")
124
126
  });
125
- var extractInputSchema = zod.z.object({
126
- instruction: zod.z.string().describe("Natural language instruction for what data to extract"),
127
- schema: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe("JSON schema defining the expected data structure (optional, will return unstructured if omitted)"),
128
- timeout: zod.z.number().optional().describe("Timeout in milliseconds")
127
+ /**
128
+ * stagehand_extract - Extract structured data from a page
129
+ */
130
+ const extractInputSchema = zod.z.object({
131
+ instruction: zod.z.string().describe("Natural language instruction for what data to extract"),
132
+ schema: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe("JSON schema defining the expected data structure (optional, will return unstructured if omitted)"),
133
+ timeout: zod.z.number().optional().describe("Timeout in milliseconds")
129
134
  });
130
- var observeInputSchema = zod.z.object({
131
- instruction: zod.z.string().optional().describe(
132
- 'Natural language instruction for what to find (e.g., "find all buttons"). If omitted, finds all interactive elements.'
133
- ),
134
- onlyVisible: zod.z.boolean().optional().describe("Only return visible elements (default: true)"),
135
- timeout: zod.z.number().optional().describe("Timeout in milliseconds")
135
+ /**
136
+ * stagehand_observe - Discover actionable elements on a page
137
+ */
138
+ const observeInputSchema = zod.z.object({
139
+ instruction: zod.z.string().optional().describe("Natural language instruction for what to find (e.g., \"find all buttons\"). If omitted, finds all interactive elements."),
140
+ onlyVisible: zod.z.boolean().optional().describe("Only return visible elements (default: true)"),
141
+ timeout: zod.z.number().optional().describe("Timeout in milliseconds")
136
142
  });
137
- var navigateInputSchema = zod.z.object({
138
- url: zod.z.string().describe("The URL to navigate to"),
139
- waitUntil: zod.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("When to consider navigation complete (default: domcontentloaded)")
143
+ /**
144
+ * stagehand_navigate - Navigate to a URL
145
+ */
146
+ const navigateInputSchema = zod.z.object({
147
+ url: zod.z.string().describe("The URL to navigate to"),
148
+ waitUntil: zod.z.enum([
149
+ "load",
150
+ "domcontentloaded",
151
+ "networkidle"
152
+ ]).optional().describe("When to consider navigation complete (default: domcontentloaded)")
140
153
  });
141
- var closeInputSchema = zod.z.object({});
142
- var tabsInputSchema = zod.z.object({
143
- action: zod.z.enum(["list", "new", "switch", "close"]).describe("Action to perform: list all tabs, open new tab, switch to tab, or close tab"),
144
- index: zod.z.number().int().min(0).optional().describe(
145
- "Tab index for switch/close actions (0-based). Required for switch, optional for close (defaults to current)."
146
- ),
147
- url: zod.z.string().optional().describe('URL to navigate to after opening new tab (optional, for "new" action only)')
154
+ /**
155
+ * stagehand_close - Close the browser
156
+ */
157
+ const closeInputSchema = zod.z.object({});
158
+ /**
159
+ * stagehand_tabs - Manage browser tabs
160
+ */
161
+ const tabsInputSchema = zod.z.object({
162
+ action: zod.z.enum([
163
+ "list",
164
+ "new",
165
+ "switch",
166
+ "close"
167
+ ]).describe("Action to perform: list all tabs, open new tab, switch to tab, or close tab"),
168
+ index: zod.z.number().int().min(0).optional().describe("Tab index for switch/close actions (0-based). Required for switch, optional for close (defaults to current)."),
169
+ url: zod.z.string().optional().describe("URL to navigate to after opening new tab (optional, for \"new\" action only)")
148
170
  }).superRefine((value, ctx) => {
149
- if (value.action === "switch" && value.index === void 0) {
150
- ctx.addIssue({
151
- code: zod.z.ZodIssueCode.custom,
152
- path: ["index"],
153
- message: 'index is required when action is "switch"'
154
- });
155
- }
156
- });
157
- var screenshotInputSchema = zod.z.object({
158
- fullPage: zod.z.boolean().optional().describe("Capture the full scrollable page instead of just the viewport (default: false)")
171
+ if (value.action === "switch" && value.index === void 0) ctx.addIssue({
172
+ code: zod.z.ZodIssueCode.custom,
173
+ path: ["index"],
174
+ message: "index is required when action is \"switch\""
175
+ });
159
176
  });
160
- var stagehandSchemas = {
161
- // Core AI
162
- act: actInputSchema,
163
- extract: extractInputSchema,
164
- observe: observeInputSchema,
165
- // Navigation & State
166
- navigate: navigateInputSchema,
167
- tabs: tabsInputSchema,
168
- close: closeInputSchema,
169
- // Utility
170
- screenshot: screenshotInputSchema
177
+ /**
178
+ * stagehand_screenshot - Capture a screenshot of the current page
179
+ */
180
+ const screenshotInputSchema = zod.z.object({ fullPage: zod.z.boolean().optional().describe("Capture the full scrollable page instead of just the viewport (default: false)") });
181
+ const stagehandSchemas = {
182
+ act: actInputSchema,
183
+ extract: extractInputSchema,
184
+ observe: observeInputSchema,
185
+ navigate: navigateInputSchema,
186
+ tabs: tabsInputSchema,
187
+ close: closeInputSchema,
188
+ screenshot: screenshotInputSchema
171
189
  };
172
-
173
- // src/tools/constants.ts
174
- var STAGEHAND_TOOLS = {
175
- // Core AI
176
- ACT: "stagehand_act",
177
- EXTRACT: "stagehand_extract",
178
- OBSERVE: "stagehand_observe",
179
- // Navigation & State
180
- NAVIGATE: "stagehand_navigate",
181
- TABS: "stagehand_tabs",
182
- CLOSE: "stagehand_close",
183
- // Utility
184
- SCREENSHOT: "stagehand_screenshot"
190
+ //#endregion
191
+ //#region src/tools/constants.ts
192
+ /**
193
+ * Stagehand Tool Constants
194
+ */
195
+ const STAGEHAND_TOOLS = {
196
+ ACT: "stagehand_act",
197
+ EXTRACT: "stagehand_extract",
198
+ OBSERVE: "stagehand_observe",
199
+ NAVIGATE: "stagehand_navigate",
200
+ TABS: "stagehand_tabs",
201
+ CLOSE: "stagehand_close",
202
+ SCREENSHOT: "stagehand_screenshot"
185
203
  };
186
-
187
- // src/tools/act.ts
204
+ //#endregion
205
+ //#region src/tools/act.ts
206
+ /**
207
+ * stagehand_act - Perform an action using natural language
208
+ */
188
209
  function createActTool(browser) {
189
- return tools.createTool({
190
- id: STAGEHAND_TOOLS.ACT,
191
- description: 'Perform an action on the page using natural language. Examples: "click the login button", "type hello into the search box", "scroll down".',
192
- inputSchema: actInputSchema,
193
- execute: async (input, { agent }) => {
194
- const threadId = agent?.threadId;
195
- browser.setCurrentThread(threadId);
196
- await browser.ensureReady();
197
- return await browser.act(input, threadId);
198
- }
199
- });
210
+ return (0, _mastra_core_tools.createTool)({
211
+ id: STAGEHAND_TOOLS.ACT,
212
+ description: "Perform an action on the page using natural language. Examples: \"click the login button\", \"type hello into the search box\", \"scroll down\".",
213
+ inputSchema: actInputSchema,
214
+ execute: async (input, { agent }) => {
215
+ const threadId = agent?.threadId;
216
+ browser.setCurrentThread(threadId);
217
+ await browser.ensureReady();
218
+ return await browser.act(input, threadId);
219
+ }
220
+ });
200
221
  }
222
+ //#endregion
223
+ //#region src/tools/close.ts
224
+ /**
225
+ * stagehand_close - Close the browser
226
+ */
201
227
  function createCloseTool(browser) {
202
- return tools.createTool({
203
- id: STAGEHAND_TOOLS.CLOSE,
204
- description: "Close the browser. Only use when done with all browsing.",
205
- inputSchema: closeInputSchema,
206
- execute: async (_input, { agent }) => {
207
- const threadId = agent?.threadId;
208
- if (browser.getScope() !== "shared") {
209
- if (!threadId) {
210
- throw new Error("stagehand_close requires agent.threadId when browser scope is not shared");
211
- }
212
- await browser.closeThreadSession(threadId);
213
- return {
214
- success: true,
215
- hint: "Thread's browser session closed. A new session will be created on next use."
216
- };
217
- }
218
- await browser.close();
219
- return {
220
- success: true,
221
- hint: "Browser closed. It will be re-launched automatically on next use."
222
- };
223
- }
224
- });
228
+ return (0, _mastra_core_tools.createTool)({
229
+ id: STAGEHAND_TOOLS.CLOSE,
230
+ description: "Close the browser. Only use when done with all browsing.",
231
+ inputSchema: closeInputSchema,
232
+ execute: async (_input, { agent }) => {
233
+ const threadId = agent?.threadId;
234
+ if (browser.getScope() !== "shared") {
235
+ if (!threadId) throw new Error("stagehand_close requires agent.threadId when browser scope is not shared");
236
+ await browser.closeThreadSession(threadId);
237
+ return {
238
+ success: true,
239
+ hint: "Thread's browser session closed. A new session will be created on next use."
240
+ };
241
+ }
242
+ await browser.close();
243
+ return {
244
+ success: true,
245
+ hint: "Browser closed. It will be re-launched automatically on next use."
246
+ };
247
+ }
248
+ });
225
249
  }
250
+ //#endregion
251
+ //#region src/tools/extract.ts
252
+ /**
253
+ * stagehand_extract - Extract structured data from a page
254
+ */
226
255
  function createExtractTool(browser) {
227
- return tools.createTool({
228
- id: STAGEHAND_TOOLS.EXTRACT,
229
- description: "Extract structured data from the page using natural language. Can optionally provide a JSON schema for the expected data structure.",
230
- inputSchema: extractInputSchema,
231
- execute: async (input, { agent }) => {
232
- const threadId = agent?.threadId;
233
- browser.setCurrentThread(threadId);
234
- await browser.ensureReady();
235
- return await browser.extract(input, threadId);
236
- }
237
- });
256
+ return (0, _mastra_core_tools.createTool)({
257
+ id: STAGEHAND_TOOLS.EXTRACT,
258
+ description: "Extract structured data from the page using natural language. Can optionally provide a JSON schema for the expected data structure.",
259
+ inputSchema: extractInputSchema,
260
+ execute: async (input, { agent }) => {
261
+ const threadId = agent?.threadId;
262
+ browser.setCurrentThread(threadId);
263
+ await browser.ensureReady();
264
+ return await browser.extract(input, threadId);
265
+ }
266
+ });
238
267
  }
268
+ //#endregion
269
+ //#region src/tools/navigate.ts
270
+ /**
271
+ * stagehand_navigate - Navigate to a URL
272
+ */
239
273
  function createNavigateTool(browser) {
240
- return tools.createTool({
241
- id: STAGEHAND_TOOLS.NAVIGATE,
242
- description: "Navigate the browser to a URL.",
243
- inputSchema: navigateInputSchema,
244
- execute: async (input, { agent }) => {
245
- const threadId = agent?.threadId;
246
- browser.setCurrentThread(threadId);
247
- await browser.ensureReady();
248
- return await browser.navigate(input, threadId);
249
- }
250
- });
274
+ return (0, _mastra_core_tools.createTool)({
275
+ id: STAGEHAND_TOOLS.NAVIGATE,
276
+ description: "Navigate the browser to a URL.",
277
+ inputSchema: navigateInputSchema,
278
+ execute: async (input, { agent }) => {
279
+ const threadId = agent?.threadId;
280
+ browser.setCurrentThread(threadId);
281
+ await browser.ensureReady();
282
+ return await browser.navigate(input, threadId);
283
+ }
284
+ });
251
285
  }
286
+ //#endregion
287
+ //#region src/tools/observe.ts
288
+ /**
289
+ * stagehand_observe - Discover actionable elements on a page
290
+ */
252
291
  function createObserveTool(browser) {
253
- return tools.createTool({
254
- id: STAGEHAND_TOOLS.OBSERVE,
255
- description: "Discover actionable elements on the page. Returns a list of actions that can be performed. Use this to understand what's on the page before acting.",
256
- inputSchema: observeInputSchema,
257
- execute: async (input, { agent }) => {
258
- const threadId = agent?.threadId;
259
- browser.setCurrentThread(threadId);
260
- await browser.ensureReady();
261
- return await browser.observe(input, threadId);
262
- }
263
- });
292
+ return (0, _mastra_core_tools.createTool)({
293
+ id: STAGEHAND_TOOLS.OBSERVE,
294
+ description: "Discover actionable elements on the page. Returns a list of actions that can be performed. Use this to understand what's on the page before acting.",
295
+ inputSchema: observeInputSchema,
296
+ execute: async (input, { agent }) => {
297
+ const threadId = agent?.threadId;
298
+ browser.setCurrentThread(threadId);
299
+ await browser.ensureReady();
300
+ return await browser.observe(input, threadId);
301
+ }
302
+ });
264
303
  }
304
+ //#endregion
305
+ //#region src/tools/screenshot.ts
306
+ /**
307
+ * stagehand_screenshot - Capture a screenshot of the current page
308
+ */
265
309
  function createScreenshotTool(browser) {
266
- return tools.createTool({
267
- id: STAGEHAND_TOOLS.SCREENSHOT,
268
- description: "Capture a screenshot of the current viewport as a visible PNG (set fullPage: true for full-page capture). Use observe or extract when you only need text or interactive elements \u2014 screenshots are expensive. Use this when you need to visually inspect the page, e.g. evaluating images, product photos, layout, design, or colors.",
269
- inputSchema: screenshotInputSchema,
270
- execute: async (input, { agent }) => {
271
- const threadId = agent?.threadId;
272
- browser.setCurrentThread(threadId);
273
- await browser.ensureReady();
274
- return await browser.screenshot(input, threadId);
275
- },
276
- toModelOutput(output) {
277
- const result = output;
278
- if (typeof result.base64 !== "string") {
279
- return {
280
- type: "content",
281
- value: [{ type: "text", text: result.message ?? "Failed to capture screenshot." }]
282
- };
283
- }
284
- return {
285
- type: "content",
286
- value: [
287
- {
288
- type: "media",
289
- mediaType: "image/png",
290
- data: result.base64
291
- }
292
- ]
293
- };
294
- }
295
- });
310
+ return (0, _mastra_core_tools.createTool)({
311
+ id: STAGEHAND_TOOLS.SCREENSHOT,
312
+ description: "Capture a screenshot of the current viewport as a visible PNG (set fullPage: true for full-page capture). Use observe or extract when you only need text or interactive elements screenshots are expensive. Use this when you need to visually inspect the page, e.g. evaluating images, product photos, layout, design, or colors.",
313
+ inputSchema: screenshotInputSchema,
314
+ execute: async (input, { agent }) => {
315
+ const threadId = agent?.threadId;
316
+ browser.setCurrentThread(threadId);
317
+ await browser.ensureReady();
318
+ return await browser.screenshot(input, threadId);
319
+ },
320
+ toModelOutput(output) {
321
+ const result = output;
322
+ if (typeof result.base64 !== "string") return {
323
+ type: "content",
324
+ value: [{
325
+ type: "text",
326
+ text: result.message ?? "Failed to capture screenshot."
327
+ }]
328
+ };
329
+ return {
330
+ type: "content",
331
+ value: [{
332
+ type: "media",
333
+ mediaType: "image/png",
334
+ data: result.base64
335
+ }]
336
+ };
337
+ }
338
+ });
296
339
  }
340
+ //#endregion
341
+ //#region src/tools/tabs.ts
342
+ /**
343
+ * stagehand_tabs - Manage browser tabs
344
+ */
297
345
  function createTabsTool(browser) {
298
- return tools.createTool({
299
- id: STAGEHAND_TOOLS.TABS,
300
- description: 'Manage browser tabs. Actions: "list" shows all tabs, "new" opens a tab (optionally with URL), "switch" changes to tab by index, "close" closes a tab.',
301
- inputSchema: tabsInputSchema,
302
- execute: async (input, { agent }) => {
303
- const threadId = agent?.threadId;
304
- browser.setCurrentThread(threadId);
305
- await browser.ensureReady();
306
- return await browser.tabs(input, threadId);
307
- }
308
- });
346
+ return (0, _mastra_core_tools.createTool)({
347
+ id: STAGEHAND_TOOLS.TABS,
348
+ description: "Manage browser tabs. Actions: \"list\" shows all tabs, \"new\" opens a tab (optionally with URL), \"switch\" changes to tab by index, \"close\" closes a tab.",
349
+ inputSchema: tabsInputSchema,
350
+ execute: async (input, { agent }) => {
351
+ const threadId = agent?.threadId;
352
+ browser.setCurrentThread(threadId);
353
+ await browser.ensureReady();
354
+ return await browser.tabs(input, threadId);
355
+ }
356
+ });
309
357
  }
310
-
311
- // src/tools/index.ts
358
+ //#endregion
359
+ //#region src/tools/index.ts
360
+ /**
361
+ * Creates all Stagehand tools bound to a StagehandBrowser instance.
362
+ * The browser is lazily initialized on first tool use.
363
+ */
312
364
  function createStagehandTools(browser) {
313
- return {
314
- // Core AI
315
- [STAGEHAND_TOOLS.ACT]: createActTool(browser),
316
- [STAGEHAND_TOOLS.EXTRACT]: createExtractTool(browser),
317
- [STAGEHAND_TOOLS.OBSERVE]: createObserveTool(browser),
318
- // Navigation & State
319
- [STAGEHAND_TOOLS.NAVIGATE]: createNavigateTool(browser),
320
- [STAGEHAND_TOOLS.TABS]: createTabsTool(browser),
321
- [STAGEHAND_TOOLS.CLOSE]: createCloseTool(browser),
322
- // Utility
323
- [STAGEHAND_TOOLS.SCREENSHOT]: createScreenshotTool(browser)
324
- };
365
+ return {
366
+ [STAGEHAND_TOOLS.ACT]: createActTool(browser),
367
+ [STAGEHAND_TOOLS.EXTRACT]: createExtractTool(browser),
368
+ [STAGEHAND_TOOLS.OBSERVE]: createObserveTool(browser),
369
+ [STAGEHAND_TOOLS.NAVIGATE]: createNavigateTool(browser),
370
+ [STAGEHAND_TOOLS.TABS]: createTabsTool(browser),
371
+ [STAGEHAND_TOOLS.CLOSE]: createCloseTool(browser),
372
+ [STAGEHAND_TOOLS.SCREENSHOT]: createScreenshotTool(browser)
373
+ };
325
374
  }
375
+ //#endregion
376
+ //#region src/utils.ts
377
+ /**
378
+ * Patch Chrome's Preferences file to set exit_type to "Normal".
379
+ *
380
+ * Stagehand uses chrome-launcher which kills Chrome with SIGKILL. This races
381
+ * with Chrome's own Preferences flush, often leaving exit_type as "Crashed".
382
+ * On next launch Chrome shows the "didn't shut down correctly" restore dialog.
383
+ *
384
+ * Safe to call even if the file doesn't exist or isn't valid JSON.
385
+ */
326
386
  function patchProfileExitType(profilePath, logger) {
327
- if (!profilePath) return;
328
- const prefsPath = path.join(profilePath, "Default", "Preferences");
329
- try {
330
- if (!fs.existsSync(prefsPath)) return;
331
- const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
332
- if (prefs?.profile?.exit_type === "Normal") return;
333
- prefs.profile = prefs.profile || {};
334
- prefs.profile.exit_type = "Normal";
335
- fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2), "utf-8");
336
- logger?.debug?.(`Patched exit_type to Normal in ${prefsPath}`);
337
- } catch {
338
- }
387
+ if (!profilePath) return;
388
+ const prefsPath = (0, path.join)(profilePath, "Default", "Preferences");
389
+ try {
390
+ if (!(0, fs.existsSync)(prefsPath)) return;
391
+ const prefs = JSON.parse((0, fs.readFileSync)(prefsPath, "utf-8"));
392
+ if (prefs?.profile?.exit_type === "Normal") return;
393
+ prefs.profile = prefs.profile || {};
394
+ prefs.profile.exit_type = "Normal";
395
+ (0, fs.writeFileSync)(prefsPath, JSON.stringify(prefs, null, 2), "utf-8");
396
+ logger?.debug?.(`Patched exit_type to Normal in ${prefsPath}`);
397
+ } catch {}
339
398
  }
399
+ /**
400
+ * Extract the Chrome process PID from a Stagehand instance.
401
+ *
402
+ * Stagehand stores the chrome-launcher result in `state.chrome` after init.
403
+ * The PID is at `chrome.process?.pid ?? chrome.pid`. This isn't part of
404
+ * Stagehand's public API, so we access it via `as any`.
405
+ *
406
+ * Returns undefined if the PID can't be found (e.g. BROWSERBASE env, not yet init'd).
407
+ */
340
408
  function getStagehandChromePid(stagehand) {
341
- try {
342
- const state = stagehand.state;
343
- if (state?.kind !== "LOCAL" || !state.chrome) return void 0;
344
- const pid = state.chrome.process?.pid ?? state.chrome.pid;
345
- return typeof pid === "number" && pid > 0 ? pid : void 0;
346
- } catch {
347
- return void 0;
348
- }
409
+ try {
410
+ const state = stagehand.state;
411
+ if (state?.kind !== "LOCAL" || !state.chrome) return void 0;
412
+ const pid = state.chrome.process?.pid ?? state.chrome.pid;
413
+ return typeof pid === "number" && pid > 0 ? pid : void 0;
414
+ } catch {
415
+ return;
416
+ }
349
417
  }
350
-
351
- // src/stagehand-browser.ts
352
- var StagehandBrowser = class extends browser.MastraBrowser {
353
- id;
354
- name = "StagehandBrowser";
355
- provider = "browserbase/stagehand";
356
- stagehandConfig;
357
- /** Debounce timers per thread for tab change reconnection */
358
- tabChangeDebounceTimers = /* @__PURE__ */ new Map();
359
- constructor(config = {}) {
360
- super(config);
361
- this.id = `stagehand-${Date.now()}`;
362
- this.stagehandConfig = config;
363
- const effectiveScope = config.cdpUrl ? config.scope ?? "shared" : config.scope ?? "thread";
364
- this.threadManager = new StagehandThreadManager({
365
- scope: effectiveScope,
366
- logger: this.logger,
367
- // When a new thread session is created, notify listeners so screencast can start
368
- onSessionCreated: (session) => {
369
- this.notifyBrowserReady(session.threadId);
370
- },
371
- // When a new browser is created for a thread, set up close listener
372
- onBrowserCreated: (stagehand, threadId) => {
373
- this.setupCloseListener(stagehand, () => this.handleThreadBrowserDisconnected(threadId), threadId);
374
- }
375
- });
376
- }
377
- /**
378
- * Ensure browser is ready and thread session exists.
379
- * For 'thread' scope, this creates a dedicated Stagehand instance for the thread.
380
- */
381
- async ensureReady() {
382
- this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
383
- await super.ensureReady();
384
- const scope = this.getScope();
385
- const threadId = this.getCurrentThread();
386
- if (scope === "thread" && threadId && threadId !== browser.DEFAULT_THREAD_ID) {
387
- await this.getManagerForThread(threadId);
388
- }
389
- }
390
- // ---------------------------------------------------------------------------
391
- // Lifecycle
392
- // ---------------------------------------------------------------------------
393
- /**
394
- * Build Stagehand options from config.
395
- * Returns the configuration object expected by Stagehand constructor.
396
- */
397
- async buildStagehandOptions() {
398
- const config = this.stagehandConfig;
399
- const stagehandOptions = {
400
- env: config.env ?? "LOCAL",
401
- model: config.model,
402
- experimental: config.experimental,
403
- disableAPI: config.disableAPI,
404
- selfHeal: config.selfHeal ?? true,
405
- domSettleTimeout: config.domSettleTimeout,
406
- verbose: config.verbose ?? 0,
407
- systemPrompt: config.systemPrompt,
408
- logger: config.logger ?? (() => {
409
- }),
410
- disablePino: config.disablePino ?? true
411
- };
412
- if (config.env === "BROWSERBASE") {
413
- if (config.apiKey) {
414
- stagehandOptions.apiKey = config.apiKey;
415
- }
416
- if (config.projectId) {
417
- stagehandOptions.projectId = config.projectId;
418
- }
419
- }
420
- if (config.profile && !fs.existsSync(config.profile)) {
421
- fs.mkdirSync(config.profile, { recursive: true });
422
- }
423
- if (config.cdpUrl && config.env !== "BROWSERBASE") {
424
- const resolvedUrl = await this.resolveCdpUrl(config.cdpUrl);
425
- const wsUrl = await this.resolveWebSocketUrl(resolvedUrl);
426
- stagehandOptions.localBrowserLaunchOptions = {
427
- cdpUrl: wsUrl,
428
- headless: this.headless,
429
- viewport: config.viewport,
430
- userDataDir: config.profile,
431
- executablePath: config.executablePath,
432
- preserveUserDataDir: config.preserveUserDataDir
433
- };
434
- } else if (config.env !== "BROWSERBASE") {
435
- stagehandOptions.localBrowserLaunchOptions = {
436
- headless: this.headless,
437
- viewport: config.viewport,
438
- userDataDir: config.profile,
439
- executablePath: config.executablePath,
440
- preserveUserDataDir: config.preserveUserDataDir
441
- };
442
- }
443
- return stagehandOptions;
444
- }
445
- /**
446
- * Create a new Stagehand instance with the current config.
447
- * Used by thread manager for 'thread' scope.
448
- */
449
- async createStagehandInstance() {
450
- const stagehandOptions = await this.buildStagehandOptions();
451
- const stagehand$1 = new stagehand.Stagehand(stagehandOptions);
452
- await stagehand$1.init();
453
- return stagehand$1;
454
- }
455
- async doLaunch() {
456
- const scope = this.getScope();
457
- this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
458
- if (scope === "thread") {
459
- return;
460
- }
461
- this.sharedManager = await this.createStagehandInstance();
462
- this.threadManager.setSharedManager(this.sharedManager);
463
- this.setupCloseListener(this.sharedManager, () => this.handleBrowserDisconnected());
464
- }
465
- /**
466
- * Set up close event listener for a shared Stagehand instance.
467
- * Listens to both context and page close events for robust detection.
468
- */
469
- /**
470
- * Set up a CDP-based close listener for a Stagehand instance.
471
- *
472
- * Tracks page targets via CDP `Target.targetCreated` / `Target.targetDestroyed`.
473
- * When all page targets are gone the `onDisconnect` callback fires. This is more
474
- * reliable than Playwright's `context.close` / `page.close` events which don't
475
- * fire when Chrome is killed externally (SIGTERM/SIGKILL).
476
- */
477
- setupCloseListener(stagehand, onDisconnect, threadId) {
478
- const chromePid = getStagehandChromePid(stagehand);
479
- if (chromePid != null) {
480
- if (threadId) {
481
- this.threadBrowserPids.set(threadId, chromePid);
482
- } else {
483
- this.sharedBrowserPid = chromePid;
484
- }
485
- }
486
- let disconnectHandled = false;
487
- const handleDisconnect = () => {
488
- if (disconnectHandled) return;
489
- disconnectHandled = true;
490
- onDisconnect();
491
- };
492
- try {
493
- const stagehandAny = stagehand;
494
- const conn = stagehandAny.ctx?.conn;
495
- if (!conn?.on) return;
496
- const pageTargets = /* @__PURE__ */ new Set();
497
- const context = stagehand.context;
498
- if (context) {
499
- for (const page of context.pages?.() ?? []) {
500
- const targetId = page._targetId ?? page.targetId;
501
- if (targetId) pageTargets.add(targetId);
502
- }
503
- }
504
- conn.on("Target.targetCreated", (params) => {
505
- if (params.targetInfo.type === "page") pageTargets.add(params.targetInfo.targetId);
506
- });
507
- conn.on("Target.targetDestroyed", (params) => {
508
- if (pageTargets.has(params.targetId)) {
509
- pageTargets.delete(params.targetId);
510
- if (pageTargets.size === 0) handleDisconnect();
511
- }
512
- });
513
- } catch {
514
- }
515
- }
516
- async doClose() {
517
- await this.threadManager.destroyAllSessions();
518
- if (this.sharedManager) {
519
- await this.sharedManager.close();
520
- this.sharedManager = null;
521
- }
522
- this.setCurrentThread(void 0);
523
- this.patchExitType();
524
- }
525
- handleBrowserDisconnected() {
526
- super.handleBrowserDisconnected();
527
- this.patchExitType();
528
- }
529
- handleThreadBrowserDisconnected(threadId) {
530
- super.handleThreadBrowserDisconnected(threadId);
531
- this.patchExitType();
532
- }
533
- async closeThreadSession(threadId) {
534
- await super.closeThreadSession(threadId);
535
- this.patchExitType();
536
- }
537
- patchExitType() {
538
- if (!this.config.profile) return;
539
- patchProfileExitType(this.config.profile, this.logger);
540
- }
541
- /**
542
- * Check if the browser is still alive by verifying the context and pages exist.
543
- * Called by base class ensureReady() to detect externally closed browsers.
544
- */
545
- async checkBrowserAlive() {
546
- const scope = this.getScope();
547
- if (scope === "thread") {
548
- return this.threadManager.hasActiveThreadManagers();
549
- }
550
- if (!this.sharedManager) {
551
- return false;
552
- }
553
- try {
554
- const context = this.sharedManager.context;
555
- if (!context) {
556
- return false;
557
- }
558
- const pages = context.pages();
559
- if (!pages || pages.length === 0) {
560
- return false;
561
- }
562
- const url = pages[0]?.url();
563
- if (url && url !== "about:blank") {
564
- const state = this.getBrowserStateFromStagehand(this.sharedManager);
565
- if (state) {
566
- this.lastBrowserState = state;
567
- }
568
- }
569
- return true;
570
- } catch (error) {
571
- const msg = error instanceof Error ? error.message : String(error);
572
- if (this.isDisconnectionError(msg)) {
573
- this.logger.debug?.("Browser was externally closed");
574
- }
575
- return false;
576
- }
577
- }
578
- /**
579
- * Create an error response from an exception.
580
- * Extends base class to add Stagehand-specific error handling.
581
- */
582
- createErrorFromException(error, context) {
583
- const msg = error instanceof Error ? error.message : String(error);
584
- if (msg.includes("No actions found") || msg.includes("Could not find")) {
585
- return this.createError(
586
- "element_not_found",
587
- `${context}: Could not find matching element or action.`,
588
- "Try rephrasing the instruction or use observe() to see available actions."
589
- );
590
- }
591
- return super.createErrorFromException(error, context);
592
- }
593
- // ---------------------------------------------------------------------------
594
- // Internal Helpers
595
- // ---------------------------------------------------------------------------
596
- /**
597
- * Get the Stagehand instance for a thread, creating it if needed.
598
- * For 'thread' scope, this creates a dedicated Stagehand instance.
599
- * For 'shared' scope, returns the shared instance.
600
- */
601
- async getManagerForThread(threadId) {
602
- const scope = this.getScope();
603
- if (scope === "shared") {
604
- return this.sharedManager;
605
- }
606
- if (!threadId || threadId === browser.DEFAULT_THREAD_ID) {
607
- return this.sharedManager;
608
- }
609
- let stagehand = this.threadManager.getExistingManagerForThread(threadId);
610
- if (!stagehand) {
611
- await this.threadManager.getManagerForThread(threadId);
612
- stagehand = this.threadManager.getExistingManagerForThread(threadId);
613
- }
614
- return stagehand ?? null;
615
- }
616
- /**
617
- * Require a Stagehand instance for the given or current thread.
618
- * Throws if no instance is available.
619
- * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
620
- * Use this to avoid race conditions in concurrent tool calls.
621
- */
622
- requireStagehand(explicitThreadId) {
623
- const threadId = explicitThreadId ?? this.getCurrentThread();
624
- const stagehand = this.threadManager.getExistingManagerForThread(threadId) ?? this.sharedManager;
625
- if (!stagehand) {
626
- throw new Error("Browser not launched");
627
- }
628
- return stagehand;
629
- }
630
- /**
631
- * Get the current page from Stagehand v3, respecting thread scope.
632
- * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
633
- * Use this to avoid race conditions in concurrent tool calls.
634
- */
635
- getPage(explicitThreadId) {
636
- const scope = this.getScope();
637
- const threadId = explicitThreadId ?? this.getCurrentThread();
638
- if (scope === "thread" && threadId && threadId !== browser.DEFAULT_THREAD_ID) {
639
- const stagehand = this.threadManager.getExistingManagerForThread(threadId);
640
- if (stagehand?.context) {
641
- return stagehand.context.activePage();
642
- }
643
- return null;
644
- }
645
- if (!this.sharedManager) return null;
646
- try {
647
- const context = this.sharedManager.context;
648
- if (context) {
649
- const activePage = context.activePage();
650
- if (activePage) {
651
- return activePage;
652
- }
653
- const pages = context.pages();
654
- if (pages && pages.length > 0) {
655
- return pages[0];
656
- }
657
- }
658
- } catch {
659
- }
660
- return null;
661
- }
662
- /**
663
- * Get the active page for a thread (implements abstract method from base class).
664
- */
665
- async getActivePage(threadId) {
666
- return this.getPage(threadId);
667
- }
668
- /**
669
- * Get a CDP session for a specific page.
670
- */
671
- getCdpSessionForPage(page) {
672
- if (!page) return null;
673
- try {
674
- const mainFrameId = page.mainFrameId?.();
675
- if (mainFrameId && page.getSessionForFrame) {
676
- return page.getSessionForFrame(mainFrameId);
677
- }
678
- } catch {
679
- }
680
- return null;
681
- }
682
- // ---------------------------------------------------------------------------
683
- // Tools - Implements MastraBrowser.getTools()
684
- // ---------------------------------------------------------------------------
685
- getTools() {
686
- const tools = createStagehandTools(this);
687
- if (this.stagehandConfig.recording) {
688
- Object.assign(tools, browser.createBrowserRecordingTools(this, this.stagehandConfig.recording));
689
- }
690
- const exclude = this.stagehandConfig.excludeTools;
691
- if (exclude?.length) {
692
- for (const name of exclude) {
693
- delete tools[name];
694
- }
695
- }
696
- return tools;
697
- }
698
- // ---------------------------------------------------------------------------
699
- // Core AI Methods
700
- // ---------------------------------------------------------------------------
701
- /**
702
- * Perform an action using natural language instruction
703
- * @param input - Action input
704
- * @param threadId - Optional thread ID for thread-safe operation
705
- */
706
- async act(input, threadId) {
707
- const stagehand = this.requireStagehand(threadId);
708
- const page = this.getPage(threadId);
709
- const url = page?.url() ?? "";
710
- try {
711
- const result = await stagehand.act(input.instruction, {
712
- variables: input.variables,
713
- timeout: input.timeout,
714
- page: page ?? void 0
715
- });
716
- return {
717
- success: result.success,
718
- message: result.message,
719
- action: result.actionDescription,
720
- url: page?.url() ?? url,
721
- hint: "Use observe() to discover available actions or extract() to get page data."
722
- };
723
- } catch (error) {
724
- return this.createErrorFromException(error, "Act");
725
- }
726
- }
727
- /**
728
- * Extract structured data from a page using natural language
729
- * @param input - Extract input
730
- * @param threadId - Optional thread ID for thread-safe operation
731
- */
732
- async extract(input, threadId) {
733
- const stagehand = this.requireStagehand(threadId);
734
- const page = this.getPage(threadId);
735
- const url = page?.url() ?? "";
736
- try {
737
- const options = { page: page ?? void 0 };
738
- const result = input.schema ? await stagehand.extract(input.instruction, input.schema, options) : await stagehand.extract(input.instruction, options);
739
- return {
740
- success: true,
741
- data: result,
742
- url: page?.url() ?? url,
743
- hint: "Data extracted successfully. Use act() to perform actions based on this data."
744
- };
745
- } catch (error) {
746
- return this.createErrorFromException(error, "Extract");
747
- }
748
- }
749
- /**
750
- * Discover actionable elements on a page
751
- * @param input - Observe input
752
- * @param threadId - Optional thread ID for thread-safe operation
753
- */
754
- async observe(input, threadId) {
755
- const stagehand = this.requireStagehand(threadId);
756
- const page = this.getPage(threadId);
757
- const url = page?.url() ?? "";
758
- try {
759
- const options = { page: page ?? void 0 };
760
- const actions = input.instruction ? await stagehand.observe(input.instruction, options) : await stagehand.observe(options);
761
- return {
762
- success: true,
763
- actions: actions.map((a) => ({
764
- selector: a.selector,
765
- description: a.description,
766
- method: a.method,
767
- arguments: a.arguments
768
- })),
769
- url: page?.url() ?? url,
770
- hint: actions.length > 0 ? `Found ${actions.length} actions. Use act() with a specific instruction to execute one.` : "No actions found. Try a different instruction or navigate to a different page."
771
- };
772
- } catch (error) {
773
- return this.createErrorFromException(error, "Observe");
774
- }
775
- }
776
- // ---------------------------------------------------------------------------
777
- // Navigation & State Methods
778
- // ---------------------------------------------------------------------------
779
- /**
780
- * Navigate to a URL
781
- * @param input - Navigate input
782
- * @param threadId - Optional thread ID for thread-safe operation
783
- */
784
- async navigate(input, threadId) {
785
- const page = this.getPage(threadId);
786
- if (!page) {
787
- return this.createError("browser_error", "Browser page not available.", "Ensure the browser is launched.");
788
- }
789
- try {
790
- await page.goto(input.url, {
791
- waitUntil: input.waitUntil ?? "domcontentloaded"
792
- });
793
- const url = page.url();
794
- const title = await page.title();
795
- return {
796
- success: true,
797
- url,
798
- title,
799
- hint: "Page loaded. Use observe() to discover actions or extract() to get data."
800
- };
801
- } catch (error) {
802
- return this.createErrorFromException(error, "Navigate");
803
- }
804
- }
805
- // ---------------------------------------------------------------------------
806
- // Screenshot
807
- // ---------------------------------------------------------------------------
808
- /**
809
- * Capture a screenshot of the current page
810
- * @param input - Screenshot input
811
- * @param threadId - Optional thread ID for thread-safe operation
812
- */
813
- async screenshot(input, threadId) {
814
- const page = this.getPage(threadId);
815
- if (!page) {
816
- return this.createError("browser_error", "Browser page not available.", "Ensure the browser is launched.");
817
- }
818
- try {
819
- const buffer = await page.screenshot({
820
- fullPage: input.fullPage ?? false,
821
- type: "png"
822
- });
823
- const base64 = Buffer.from(buffer).toString("base64");
824
- const url = page.url();
825
- const title = await page.title();
826
- return { base64, url, title };
827
- } catch (error) {
828
- return this.createErrorFromException(error, "Screenshot");
829
- }
830
- }
831
- // ---------------------------------------------------------------------------
832
- // Tab Management
833
- // ---------------------------------------------------------------------------
834
- /**
835
- * Manage browser tabs - list, create, switch, close
836
- * @param input - Tabs input
837
- * @param threadId - Optional thread ID for thread-safe operation
838
- */
839
- async tabs(input, threadId) {
840
- const effectiveThreadId = threadId ?? this.getCurrentThread();
841
- const stagehand = this.requireStagehand(effectiveThreadId);
842
- const context = stagehand.context;
843
- if (!context) {
844
- return this.createError("browser_error", "Browser context not available.", "Ensure the browser is launched.");
845
- }
846
- try {
847
- switch (input.action) {
848
- case "list": {
849
- const pages = context.pages();
850
- const activePage = context.activePage();
851
- const tabs = await Promise.all(
852
- pages.map(async (page, index) => ({
853
- index,
854
- url: page.url(),
855
- title: await page.title(),
856
- active: page === activePage
857
- }))
858
- );
859
- return {
860
- success: true,
861
- tabs,
862
- hint: 'Use stagehand_tabs with action:"switch" and index to change tabs.'
863
- };
864
- }
865
- case "new": {
866
- const newPage = await context.newPage(input.url);
867
- await this.reconnectScreencastForThread(effectiveThreadId, "new tab via tool");
868
- this.updateSessionBrowserState(effectiveThreadId);
869
- return {
870
- success: true,
871
- index: context.pages().length - 1,
872
- url: newPage.url(),
873
- title: await newPage.title(),
874
- hint: "New tab opened. Use stagehand_observe to discover actions."
875
- };
876
- }
877
- case "switch": {
878
- if (input.index === void 0) {
879
- return this.createError(
880
- "browser_error",
881
- "Tab index required for switch action.",
882
- "Provide index parameter."
883
- );
884
- }
885
- const pages = context.pages();
886
- if (input.index < 0 || input.index >= pages.length) {
887
- return this.createError(
888
- "browser_error",
889
- `Invalid tab index: ${input.index}. Valid range: 0-${pages.length - 1}`,
890
- 'Use stagehand_tabs with action:"list" to see available tabs.'
891
- );
892
- }
893
- const targetPage = pages[input.index];
894
- const targetUrl = targetPage.url();
895
- context.setActivePage(targetPage);
896
- await this.reconnectScreencastForThread(effectiveThreadId, "tab switch via tool");
897
- const streamKey = this.getStreamKey(effectiveThreadId);
898
- const stream = this.activeScreencastStreams.get(streamKey);
899
- if (targetUrl && stream?.isActive()) {
900
- stream.emitUrl(targetUrl);
901
- }
902
- this.updateSessionBrowserState(effectiveThreadId);
903
- return {
904
- success: true,
905
- index: input.index,
906
- url: targetUrl,
907
- title: await targetPage.title(),
908
- hint: "Tab switched. Use stagehand_observe to discover actions."
909
- };
910
- }
911
- case "close": {
912
- const pages = context.pages();
913
- const indexToClose = input.index ?? pages.findIndex((p) => p === context.activePage());
914
- if (indexToClose < 0 || indexToClose >= pages.length) {
915
- return this.createError(
916
- "browser_error",
917
- `Invalid tab index: ${indexToClose}`,
918
- 'Use stagehand_tabs with action:"list" to see available tabs.'
919
- );
920
- }
921
- const pageToClose = pages[indexToClose];
922
- await pageToClose.close();
923
- await this.reconnectScreencastForThread(effectiveThreadId, "tab close via tool");
924
- this.updateSessionBrowserState(effectiveThreadId);
925
- const remainingPages = context.pages();
926
- return {
927
- success: true,
928
- remaining: remainingPages.length,
929
- hint: remainingPages.length > 0 ? "Tab closed. Use stagehand_observe to see current tab." : "All tabs closed."
930
- };
931
- }
932
- default:
933
- return this.createError(
934
- "browser_error",
935
- `Unknown tabs action: ${input.action}`,
936
- 'Use "list", "new", "switch", or "close".'
937
- );
938
- }
939
- } catch (error) {
940
- return this.createErrorFromException(error, "Tabs");
941
- }
942
- }
943
- // ---------------------------------------------------------------------------
944
- // URL Tracking (for Studio browser view)
945
- // ---------------------------------------------------------------------------
946
- async getCurrentUrl(threadId) {
947
- if (!this.isBrowserRunning()) {
948
- return null;
949
- }
950
- const effectiveThreadId = threadId ?? this.getCurrentThread();
951
- const scope = this.threadManager.getScope();
952
- if (scope === "thread" && effectiveThreadId) {
953
- const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
954
- if (!stagehand?.context) {
955
- return null;
956
- }
957
- const page2 = stagehand.context.activePage();
958
- const url = page2?.url() ?? null;
959
- if (url && url !== "about:blank") {
960
- const state = this.getBrowserStateFromStagehand(stagehand);
961
- if (state) {
962
- this.threadManager.updateBrowserState(effectiveThreadId, state);
963
- }
964
- }
965
- return url;
966
- }
967
- const page = this.getPage();
968
- if (!page) return null;
969
- try {
970
- const url = page.url();
971
- if (url && url !== "about:blank") {
972
- const state = this.getBrowserStateFromStagehand(this.sharedManager);
973
- if (state) {
974
- this.lastBrowserState = state;
975
- }
976
- }
977
- return url;
978
- } catch {
979
- return null;
980
- }
981
- }
982
- /**
983
- * Navigate to a URL (simple version). Used internally for restoring state on relaunch.
984
- */
985
- async navigateTo(url) {
986
- const page = this.getPage();
987
- if (!page) return;
988
- try {
989
- await page.goto(url, {
990
- timeoutMs: this.config.timeout ?? 3e4,
991
- waitUntil: "domcontentloaded"
992
- });
993
- } catch {
994
- }
995
- }
996
- /**
997
- * Get the current browser state (all tabs and active tab index).
998
- */
999
- async getBrowserState(threadId) {
1000
- if (!this.isBrowserRunning()) {
1001
- return null;
1002
- }
1003
- try {
1004
- const scope = this.threadManager.getScope();
1005
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1006
- if (scope === "thread" && effectiveThreadId) {
1007
- const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1008
- if (!stagehand) return null;
1009
- return this.getBrowserStateFromStagehand(stagehand);
1010
- }
1011
- return this.getBrowserStateFromStagehand(this.sharedManager);
1012
- } catch {
1013
- return null;
1014
- }
1015
- }
1016
- /**
1017
- * Get browser state for a thread (implements abstract method from base class).
1018
- * Sync version that uses existing manager lookup without creating sessions.
1019
- */
1020
- getBrowserStateForThread(threadId) {
1021
- const effectiveThreadId = threadId ?? this.getCurrentThread() ?? browser.DEFAULT_THREAD_ID;
1022
- const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1023
- return this.getBrowserStateFromStagehand(stagehand);
1024
- }
1025
- /**
1026
- * Get browser state from a specific Stagehand instance.
1027
- */
1028
- getBrowserStateFromStagehand(stagehand) {
1029
- if (!stagehand?.context) return null;
1030
- try {
1031
- const pages = stagehand.context.pages();
1032
- const activePage = stagehand.context.activePage();
1033
- let activeIndex = 0;
1034
- const tabs = pages.map((page, index) => {
1035
- if (page === activePage) {
1036
- activeIndex = index;
1037
- }
1038
- return { url: page.url() };
1039
- });
1040
- return {
1041
- tabs,
1042
- activeTabIndex: activeIndex
1043
- };
1044
- } catch {
1045
- return null;
1046
- }
1047
- }
1048
- /**
1049
- * Get all open tabs with their URLs and titles.
1050
- */
1051
- async getTabState(threadId) {
1052
- const state = await this.getBrowserState(threadId);
1053
- return state?.tabs ?? [];
1054
- }
1055
- /**
1056
- * Get the active tab index.
1057
- */
1058
- async getActiveTabIndex(threadId) {
1059
- const state = await this.getBrowserState(threadId);
1060
- return state?.activeTabIndex ?? 0;
1061
- }
1062
- // ---------------------------------------------------------------------------
1063
- // Screencast (for Studio live view)
1064
- // Uses Stagehand v3's native CDP access
1065
- // ---------------------------------------------------------------------------
1066
- async startScreencast(options) {
1067
- const threadId = options?.threadId;
1068
- const provider = {
1069
- getCdpSession: async () => {
1070
- const page = await this.threadManager.getPageForThread(threadId);
1071
- if (!page) {
1072
- throw new Error("No page available for screencast");
1073
- }
1074
- const session = this.getCdpSessionForPage(page);
1075
- if (!session) {
1076
- throw new Error("No CDP session available for page");
1077
- }
1078
- return session;
1079
- },
1080
- isBrowserRunning: () => this.isBrowserRunning()
1081
- };
1082
- const stream = new browser.ScreencastStreamImpl(provider, options);
1083
- const streamKey = this.getStreamKey(threadId);
1084
- this.activeScreencastStreams.set(streamKey, stream);
1085
- await stream.start();
1086
- await this.setupTabChangeDetection(threadId, stream);
1087
- stream.once("stop", () => {
1088
- if (this.activeScreencastStreams.get(streamKey) === stream) {
1089
- this.activeScreencastStreams.delete(streamKey);
1090
- }
1091
- const timer = this.tabChangeDebounceTimers.get(streamKey);
1092
- if (timer) {
1093
- clearTimeout(timer);
1094
- this.tabChangeDebounceTimers.delete(streamKey);
1095
- }
1096
- });
1097
- return stream;
1098
- }
1099
- /**
1100
- * Set up listeners to detect tab changes and reconnect the screencast.
1101
- * Uses CDP Target events since Stagehand doesn't expose page lifecycle events.
1102
- */
1103
- async setupTabChangeDetection(threadId, stream) {
1104
- const stagehand = await this.getManagerForThread(threadId);
1105
- if (!stagehand?.context) return;
1106
- const connection = stagehand.context.conn;
1107
- if (!connection) {
1108
- this.logger.debug?.("No CDP connection available for tab change detection");
1109
- return;
1110
- }
1111
- const targetSessions = /* @__PURE__ */ new Map();
1112
- let targetInfoDebounceTimer = null;
1113
- const isTrackedByStagehand = (targetId) => {
1114
- const pages = stagehand.context?.pages() || [];
1115
- return pages.some((p) => p.targetId() === targetId);
1116
- };
1117
- const streamKey = this.getStreamKey(threadId);
1118
- const onTargetCreated = (params) => {
1119
- if (params.targetInfo.type !== "page") return;
1120
- const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1121
- if (existingTimer) {
1122
- clearTimeout(existingTimer);
1123
- }
1124
- this.tabChangeDebounceTimers.set(
1125
- streamKey,
1126
- setTimeout(() => {
1127
- this.tabChangeDebounceTimers.delete(streamKey);
1128
- void this.reconnectScreencastForThread(threadId, "new tab");
1129
- void setupPageNavigationListener();
1130
- }, 300)
1131
- );
1132
- };
1133
- const onTargetAttached = (params) => {
1134
- if (params.targetInfo.type !== "page") return;
1135
- targetSessions.set(params.targetInfo.targetId, params.sessionId);
1136
- };
1137
- let pendingTargetInfo = null;
1138
- const onTargetInfoChanged = (params) => {
1139
- if (params.targetInfo.type !== "page") return;
1140
- if (isTrackedByStagehand(params.targetInfo.targetId)) return;
1141
- const sessionId = targetSessions.get(params.targetInfo.targetId);
1142
- if (!sessionId) return;
1143
- pendingTargetInfo = params;
1144
- if (targetInfoDebounceTimer) {
1145
- clearTimeout(targetInfoDebounceTimer);
1146
- }
1147
- targetInfoDebounceTimer = setTimeout(async () => {
1148
- targetInfoDebounceTimer = null;
1149
- if (!pendingTargetInfo) return;
1150
- const info = pendingTargetInfo.targetInfo;
1151
- const sid = targetSessions.get(info.targetId);
1152
- pendingTargetInfo = null;
1153
- if (isTrackedByStagehand(info.targetId) || !sid) return;
1154
- const contextAny = stagehand.context;
1155
- if (contextAny?.onAttachedToTarget) {
1156
- try {
1157
- await contextAny.onAttachedToTarget(info, sid);
1158
- await new Promise((resolve) => setTimeout(resolve, 100));
1159
- if (isTrackedByStagehand(info.targetId)) {
1160
- this.logger.debug?.("Page registered successfully, setting as active");
1161
- const pages = stagehand.context?.pages() || [];
1162
- const newPage = pages.find((p) => p.targetId() === info.targetId);
1163
- if (newPage && stagehand.context) {
1164
- stagehand.context.setActivePage(newPage);
1165
- }
1166
- void this.reconnectScreencastForThread(threadId, "manual tab tracked");
1167
- void setupPageNavigationListener();
1168
- } else {
1169
- this.logger.debug?.("Stagehand did not register the page (non-injectable URL)");
1170
- }
1171
- } catch (e) {
1172
- this.logger.debug?.("Failed to register page with Stagehand", e);
1173
- }
1174
- }
1175
- }, 300);
1176
- };
1177
- const onTargetDestroyed = (params) => {
1178
- this.logger.debug?.("Page target destroyed");
1179
- targetSessions.delete(params.targetId);
1180
- const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1181
- if (existingTimer) {
1182
- clearTimeout(existingTimer);
1183
- }
1184
- this.tabChangeDebounceTimers.set(
1185
- streamKey,
1186
- setTimeout(() => {
1187
- this.tabChangeDebounceTimers.delete(streamKey);
1188
- void this.reconnectScreencastForThread(threadId, "tab closed");
1189
- void setupPageNavigationListener();
1190
- }, 300)
1191
- );
1192
- };
1193
- const onFrameNavigated = (params) => {
1194
- if (!params.frame.parentId && params.frame.url) {
1195
- stream.emitUrl(params.frame.url);
1196
- this.updateSessionBrowserState(threadId);
1197
- const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1198
- if (existingTimer) {
1199
- clearTimeout(existingTimer);
1200
- }
1201
- this.tabChangeDebounceTimers.set(
1202
- streamKey,
1203
- setTimeout(() => {
1204
- this.tabChangeDebounceTimers.delete(streamKey);
1205
- void this.reconnectScreencastForThread(threadId, "same-tab navigation");
1206
- }, 300)
1207
- );
1208
- }
1209
- };
1210
- let pageSession = null;
1211
- const setupPageNavigationListener = async () => {
1212
- try {
1213
- if (pageSession?.off) {
1214
- pageSession.off("Page.frameNavigated", onFrameNavigated);
1215
- }
1216
- const page = stagehand.context?.activePage();
1217
- if (!page) return;
1218
- const session = page.getSessionForFrame(page.mainFrameId());
1219
- if (!session) return;
1220
- pageSession = session;
1221
- await session.send("Page.enable");
1222
- session.on("Page.frameNavigated", onFrameNavigated);
1223
- const currentUrl = page.url();
1224
- if (currentUrl && currentUrl !== "about:blank") {
1225
- stream.emitUrl(currentUrl);
1226
- }
1227
- } catch (error) {
1228
- this.logger.debug?.("Failed to set up page navigation listener", error);
1229
- }
1230
- };
1231
- const cleanup = () => {
1232
- const timer = this.tabChangeDebounceTimers.get(streamKey);
1233
- if (timer) {
1234
- clearTimeout(timer);
1235
- this.tabChangeDebounceTimers.delete(streamKey);
1236
- }
1237
- if (targetInfoDebounceTimer) {
1238
- clearTimeout(targetInfoDebounceTimer);
1239
- targetInfoDebounceTimer = null;
1240
- }
1241
- connection.off?.("Target.targetCreated", onTargetCreated);
1242
- connection.off?.("Target.targetDestroyed", onTargetDestroyed);
1243
- connection.off?.("Target.attachedToTarget", onTargetAttached);
1244
- connection.off?.("Target.targetInfoChanged", onTargetInfoChanged);
1245
- if (pageSession?.off) {
1246
- pageSession.off("Page.frameNavigated", onFrameNavigated);
1247
- }
1248
- };
1249
- stream.once("stop", cleanup);
1250
- try {
1251
- connection.on?.("Target.targetCreated", onTargetCreated);
1252
- connection.on?.("Target.targetDestroyed", onTargetDestroyed);
1253
- connection.on?.("Target.attachedToTarget", onTargetAttached);
1254
- connection.on?.("Target.targetInfoChanged", onTargetInfoChanged);
1255
- await setupPageNavigationListener();
1256
- } catch (error) {
1257
- this.logger.debug?.("Failed to set up tab change detection", error);
1258
- }
1259
- }
1260
- // NOTE: Manual tab switching in browser UI is not fully supported.
1261
- // Stagehand v3 does not track pages opened via browser UI (only pages created through its API).
1262
- // We've requested this feature from Browserbase - see Notion doc for details.
1263
- // ---------------------------------------------------------------------------
1264
- // Event Injection (for Studio live view interactivity)
1265
- // ---------------------------------------------------------------------------
1266
- async injectMouseEvent(event, threadId) {
1267
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1268
- const page = await this.threadManager.getPageForThread(effectiveThreadId);
1269
- const cdpSession = this.getCdpSessionForPage(page);
1270
- if (!cdpSession) {
1271
- throw new Error("No CDP session available");
1272
- }
1273
- const buttonMap = {
1274
- none: 0,
1275
- left: 1,
1276
- middle: 4,
1277
- right: 2
1278
- };
1279
- const defaultClickCount = event.type === "mousePressed" || event.type === "mouseReleased" ? 1 : 0;
1280
- await cdpSession.send("Input.dispatchMouseEvent", {
1281
- type: event.type,
1282
- x: event.x,
1283
- y: event.y,
1284
- button: event.button ?? "none",
1285
- buttons: buttonMap[event.button ?? "none"] ?? 0,
1286
- clickCount: event.clickCount ?? defaultClickCount,
1287
- deltaX: event.deltaX ?? 0,
1288
- deltaY: event.deltaY ?? 0,
1289
- modifiers: event.modifiers ?? 0
1290
- });
1291
- }
1292
- async injectKeyboardEvent(event, threadId) {
1293
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1294
- const page = await this.threadManager.getPageForThread(effectiveThreadId);
1295
- const cdpSession = this.getCdpSessionForPage(page);
1296
- if (!cdpSession) {
1297
- throw new Error("No CDP session available");
1298
- }
1299
- await cdpSession.send("Input.dispatchKeyEvent", {
1300
- type: event.type,
1301
- key: event.key,
1302
- code: event.code,
1303
- text: event.text,
1304
- modifiers: event.modifiers ?? 0,
1305
- windowsVirtualKeyCode: event.windowsVirtualKeyCode
1306
- });
1307
- }
418
+ //#endregion
419
+ //#region src/stagehand-browser.ts
420
+ /**
421
+ * StagehandBrowser - AI-powered browser automation using Stagehand v3
422
+ *
423
+ * Uses natural language instructions for browser interactions.
424
+ * Fundamentally different from AgentBrowser's deterministic refs approach.
425
+ *
426
+ * Stagehand v3 is CDP-native and provides direct CDP access for screencast/input injection.
427
+ */
428
+ /**
429
+ * StagehandBrowser - AI-powered browser using Stagehand v3
430
+ *
431
+ * Unlike AgentBrowser which uses refs ([ref=e1]), StagehandBrowser uses
432
+ * natural language instructions for all interactions.
433
+ *
434
+ * Supports thread scope via the scope config:
435
+ * - 'shared': All threads share the same Stagehand instance
436
+ * - 'thread': Each thread gets its own Stagehand instance (separate browser)
437
+ */
438
+ var StagehandBrowser = class extends _mastra_core_browser.MastraBrowser {
439
+ id;
440
+ name = "StagehandBrowser";
441
+ provider = "browserbase/stagehand";
442
+ stagehandConfig;
443
+ /** Debounce timers per thread for tab change reconnection */
444
+ tabChangeDebounceTimers = /* @__PURE__ */ new Map();
445
+ constructor(config = {}) {
446
+ super(config);
447
+ this.id = `stagehand-${Date.now()}`;
448
+ this.stagehandConfig = config;
449
+ const effectiveScope = config.cdpUrl ? config.scope ?? "shared" : config.scope ?? "thread";
450
+ this.threadManager = new StagehandThreadManager({
451
+ scope: effectiveScope,
452
+ logger: this.logger,
453
+ onSessionCreated: (session) => {
454
+ this.notifyBrowserReady(session.threadId);
455
+ },
456
+ onBrowserCreated: (stagehand, threadId) => {
457
+ this.setupCloseListener(stagehand, () => this.handleThreadBrowserDisconnected(threadId), threadId);
458
+ }
459
+ });
460
+ }
461
+ /**
462
+ * Ensure browser is ready and thread session exists.
463
+ * For 'thread' scope, this creates a dedicated Stagehand instance for the thread.
464
+ */
465
+ async ensureReady() {
466
+ this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
467
+ await super.ensureReady();
468
+ const scope = this.getScope();
469
+ const threadId = this.getCurrentThread();
470
+ if (scope === "thread" && threadId && threadId !== _mastra_core_browser.DEFAULT_THREAD_ID) await this.getManagerForThread(threadId);
471
+ }
472
+ /**
473
+ * Build Stagehand options from config.
474
+ * Returns the configuration object expected by Stagehand constructor.
475
+ */
476
+ async buildStagehandOptions() {
477
+ const config = this.stagehandConfig;
478
+ const stagehandOptions = {
479
+ env: config.env ?? "LOCAL",
480
+ model: config.model,
481
+ experimental: config.experimental,
482
+ disableAPI: config.disableAPI,
483
+ selfHeal: config.selfHeal ?? true,
484
+ domSettleTimeout: config.domSettleTimeout,
485
+ verbose: config.verbose ?? 0,
486
+ systemPrompt: config.systemPrompt,
487
+ logger: config.logger ?? (() => {}),
488
+ disablePino: config.disablePino ?? true
489
+ };
490
+ if (config.env === "BROWSERBASE") {
491
+ if (config.apiKey) stagehandOptions.apiKey = config.apiKey;
492
+ if (config.projectId) stagehandOptions.projectId = config.projectId;
493
+ }
494
+ if (config.profile && !(0, fs.existsSync)(config.profile)) (0, fs.mkdirSync)(config.profile, { recursive: true });
495
+ if (config.cdpUrl && config.env !== "BROWSERBASE") {
496
+ const resolvedUrl = await this.resolveCdpUrl(config.cdpUrl);
497
+ stagehandOptions.localBrowserLaunchOptions = {
498
+ cdpUrl: await this.resolveWebSocketUrl(resolvedUrl),
499
+ headless: this.headless,
500
+ viewport: (0, _mastra_core_browser.resolveViewportSize)(config.viewport),
501
+ userDataDir: config.profile,
502
+ executablePath: config.executablePath,
503
+ preserveUserDataDir: config.preserveUserDataDir
504
+ };
505
+ } else if (config.env !== "BROWSERBASE") stagehandOptions.localBrowserLaunchOptions = {
506
+ headless: this.headless,
507
+ viewport: (0, _mastra_core_browser.resolveViewportSize)(config.viewport) ?? _mastra_core_browser.DEFAULT_BROWSER_VIEWPORT,
508
+ userDataDir: config.profile,
509
+ executablePath: config.executablePath,
510
+ preserveUserDataDir: config.preserveUserDataDir
511
+ };
512
+ return stagehandOptions;
513
+ }
514
+ /**
515
+ * Create a new Stagehand instance with the current config.
516
+ * Used by thread manager for 'thread' scope.
517
+ */
518
+ async createStagehandInstance() {
519
+ const stagehand = new _browserbasehq_stagehand.Stagehand(await this.buildStagehandOptions());
520
+ await stagehand.init();
521
+ return stagehand;
522
+ }
523
+ async doLaunch() {
524
+ const scope = this.getScope();
525
+ this.threadManager.setCreateStagehand(() => this.createStagehandInstance());
526
+ if (scope === "thread") return;
527
+ this.sharedManager = await this.createStagehandInstance();
528
+ this.threadManager.setSharedManager(this.sharedManager);
529
+ this.setupCloseListener(this.sharedManager, () => this.handleBrowserDisconnected());
530
+ }
531
+ /**
532
+ * Set up close event listener for a shared Stagehand instance.
533
+ * Listens to both context and page close events for robust detection.
534
+ */
535
+ /**
536
+ * Set up a CDP-based close listener for a Stagehand instance.
537
+ *
538
+ * Tracks page targets via CDP `Target.targetCreated` / `Target.targetDestroyed`.
539
+ * When all page targets are gone the `onDisconnect` callback fires. This is more
540
+ * reliable than Playwright's `context.close` / `page.close` events which don't
541
+ * fire when Chrome is killed externally (SIGTERM/SIGKILL).
542
+ */
543
+ setupCloseListener(stagehand, onDisconnect, threadId) {
544
+ const chromePid = getStagehandChromePid(stagehand);
545
+ if (chromePid != null) if (threadId) this.threadBrowserPids.set(threadId, chromePid);
546
+ else this.sharedBrowserPid = chromePid;
547
+ let disconnectHandled = false;
548
+ const handleDisconnect = () => {
549
+ if (disconnectHandled) return;
550
+ disconnectHandled = true;
551
+ onDisconnect();
552
+ };
553
+ try {
554
+ const conn = stagehand.ctx?.conn;
555
+ if (!conn?.on) return;
556
+ const pageTargets = /* @__PURE__ */ new Set();
557
+ const context = stagehand.context;
558
+ if (context) for (const page of context.pages?.() ?? []) {
559
+ const targetId = page._targetId ?? page.targetId;
560
+ if (targetId) pageTargets.add(targetId);
561
+ }
562
+ conn.on("Target.targetCreated", (params) => {
563
+ if (params.targetInfo.type === "page") pageTargets.add(params.targetInfo.targetId);
564
+ });
565
+ conn.on("Target.targetDestroyed", (params) => {
566
+ if (pageTargets.has(params.targetId)) {
567
+ pageTargets.delete(params.targetId);
568
+ if (pageTargets.size === 0) handleDisconnect();
569
+ }
570
+ });
571
+ } catch {}
572
+ }
573
+ async doClose() {
574
+ await this.threadManager.destroyAllSessions();
575
+ if (this.sharedManager) {
576
+ await this.sharedManager.close();
577
+ this.sharedManager = null;
578
+ }
579
+ this.setCurrentThread(void 0);
580
+ this.patchExitType();
581
+ }
582
+ handleBrowserDisconnected() {
583
+ super.handleBrowserDisconnected();
584
+ this.patchExitType();
585
+ }
586
+ handleThreadBrowserDisconnected(threadId) {
587
+ super.handleThreadBrowserDisconnected(threadId);
588
+ this.patchExitType();
589
+ }
590
+ async closeThreadSession(threadId) {
591
+ await super.closeThreadSession(threadId);
592
+ this.patchExitType();
593
+ }
594
+ patchExitType() {
595
+ if (!this.config.profile) return;
596
+ patchProfileExitType(this.config.profile, this.logger);
597
+ }
598
+ /**
599
+ * Check if the browser is still alive by verifying the context and pages exist.
600
+ * Called by base class ensureReady() to detect externally closed browsers.
601
+ */
602
+ async checkBrowserAlive() {
603
+ if (this.getScope() === "thread") return this.threadManager.hasActiveThreadManagers();
604
+ if (!this.sharedManager) return false;
605
+ try {
606
+ const context = this.sharedManager.context;
607
+ if (!context) return false;
608
+ const pages = context.pages();
609
+ if (!pages || pages.length === 0) return false;
610
+ const url = pages[0]?.url();
611
+ if (url && url !== "about:blank") {
612
+ const state = this.getBrowserStateFromStagehand(this.sharedManager);
613
+ if (state) this.lastBrowserState = state;
614
+ }
615
+ return true;
616
+ } catch (error) {
617
+ const msg = error instanceof Error ? error.message : String(error);
618
+ if (this.isDisconnectionError(msg)) this.logger.debug?.("Browser was externally closed");
619
+ return false;
620
+ }
621
+ }
622
+ /**
623
+ * Create an error response from an exception.
624
+ * Extends base class to add Stagehand-specific error handling.
625
+ */
626
+ createErrorFromException(error, context) {
627
+ const msg = error instanceof Error ? error.message : String(error);
628
+ if (msg.includes("No actions found") || msg.includes("Could not find")) return this.createError("element_not_found", `${context}: Could not find matching element or action.`, "Try rephrasing the instruction or use observe() to see available actions.");
629
+ return super.createErrorFromException(error, context);
630
+ }
631
+ /**
632
+ * Get the Stagehand instance for a thread, creating it if needed.
633
+ * For 'thread' scope, this creates a dedicated Stagehand instance.
634
+ * For 'shared' scope, returns the shared instance.
635
+ */
636
+ async getManagerForThread(threadId) {
637
+ if (this.getScope() === "shared") return this.sharedManager;
638
+ if (!threadId || threadId === _mastra_core_browser.DEFAULT_THREAD_ID) return this.sharedManager;
639
+ let stagehand = this.threadManager.getExistingManagerForThread(threadId);
640
+ if (!stagehand) {
641
+ await this.threadManager.getManagerForThread(threadId);
642
+ stagehand = this.threadManager.getExistingManagerForThread(threadId);
643
+ }
644
+ return stagehand ?? null;
645
+ }
646
+ /**
647
+ * Require a Stagehand instance for the given or current thread.
648
+ * Throws if no instance is available.
649
+ * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
650
+ * Use this to avoid race conditions in concurrent tool calls.
651
+ */
652
+ requireStagehand(explicitThreadId) {
653
+ const threadId = explicitThreadId ?? this.getCurrentThread();
654
+ const stagehand = this.threadManager.getExistingManagerForThread(threadId) ?? this.sharedManager;
655
+ if (!stagehand) throw new Error("Browser not launched");
656
+ return stagehand;
657
+ }
658
+ /**
659
+ * Get the current page from Stagehand v3, respecting thread scope.
660
+ * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
661
+ * Use this to avoid race conditions in concurrent tool calls.
662
+ */
663
+ getPage(explicitThreadId) {
664
+ const scope = this.getScope();
665
+ const threadId = explicitThreadId ?? this.getCurrentThread();
666
+ if (scope === "thread" && threadId && threadId !== _mastra_core_browser.DEFAULT_THREAD_ID) {
667
+ const stagehand = this.threadManager.getExistingManagerForThread(threadId);
668
+ if (stagehand?.context) return stagehand.context.activePage();
669
+ return null;
670
+ }
671
+ if (!this.sharedManager) return null;
672
+ try {
673
+ const context = this.sharedManager.context;
674
+ if (context) {
675
+ const activePage = context.activePage();
676
+ if (activePage) return activePage;
677
+ const pages = context.pages();
678
+ if (pages && pages.length > 0) return pages[0];
679
+ }
680
+ } catch {}
681
+ return null;
682
+ }
683
+ /**
684
+ * Get the active page for a thread (implements abstract method from base class).
685
+ */
686
+ async getActivePage(threadId) {
687
+ return this.getPage(threadId);
688
+ }
689
+ /**
690
+ * Get a CDP session for a specific page.
691
+ */
692
+ getCdpSessionForPage(page) {
693
+ if (!page) return null;
694
+ try {
695
+ const mainFrameId = page.mainFrameId?.();
696
+ if (mainFrameId && page.getSessionForFrame) return page.getSessionForFrame(mainFrameId);
697
+ } catch {}
698
+ return null;
699
+ }
700
+ getTools() {
701
+ const tools = createStagehandTools(this);
702
+ if (this.stagehandConfig.recording) Object.assign(tools, (0, _mastra_core_browser.createBrowserRecordingTools)(this, this.stagehandConfig.recording));
703
+ const exclude = this.stagehandConfig.excludeTools;
704
+ if (exclude?.length) for (const name of exclude) delete tools[name];
705
+ return tools;
706
+ }
707
+ /**
708
+ * Perform an action using natural language instruction
709
+ * @param input - Action input
710
+ * @param threadId - Optional thread ID for thread-safe operation
711
+ */
712
+ async act(input, threadId) {
713
+ const stagehand = this.requireStagehand(threadId);
714
+ const page = this.getPage(threadId);
715
+ const url = page?.url() ?? "";
716
+ try {
717
+ const result = await stagehand.act(input.instruction, {
718
+ variables: input.variables,
719
+ timeout: input.timeout,
720
+ page: page ?? void 0
721
+ });
722
+ return {
723
+ success: result.success,
724
+ message: result.message,
725
+ action: result.actionDescription,
726
+ url: page?.url() ?? url,
727
+ hint: "Use observe() to discover available actions or extract() to get page data."
728
+ };
729
+ } catch (error) {
730
+ return this.createErrorFromException(error, "Act");
731
+ }
732
+ }
733
+ /**
734
+ * Extract structured data from a page using natural language
735
+ * @param input - Extract input
736
+ * @param threadId - Optional thread ID for thread-safe operation
737
+ */
738
+ async extract(input, threadId) {
739
+ const stagehand = this.requireStagehand(threadId);
740
+ const page = this.getPage(threadId);
741
+ const url = page?.url() ?? "";
742
+ try {
743
+ const options = { page: page ?? void 0 };
744
+ return {
745
+ success: true,
746
+ data: input.schema ? await stagehand.extract(input.instruction, input.schema, options) : await stagehand.extract(input.instruction, options),
747
+ url: page?.url() ?? url,
748
+ hint: "Data extracted successfully. Use act() to perform actions based on this data."
749
+ };
750
+ } catch (error) {
751
+ return this.createErrorFromException(error, "Extract");
752
+ }
753
+ }
754
+ /**
755
+ * Discover actionable elements on a page
756
+ * @param input - Observe input
757
+ * @param threadId - Optional thread ID for thread-safe operation
758
+ */
759
+ async observe(input, threadId) {
760
+ const stagehand = this.requireStagehand(threadId);
761
+ const page = this.getPage(threadId);
762
+ const url = page?.url() ?? "";
763
+ try {
764
+ const options = { page: page ?? void 0 };
765
+ const actions = input.instruction ? await stagehand.observe(input.instruction, options) : await stagehand.observe(options);
766
+ return {
767
+ success: true,
768
+ actions: actions.map((a) => ({
769
+ selector: a.selector,
770
+ description: a.description,
771
+ method: a.method,
772
+ arguments: a.arguments
773
+ })),
774
+ url: page?.url() ?? url,
775
+ hint: actions.length > 0 ? `Found ${actions.length} actions. Use act() with a specific instruction to execute one.` : "No actions found. Try a different instruction or navigate to a different page."
776
+ };
777
+ } catch (error) {
778
+ return this.createErrorFromException(error, "Observe");
779
+ }
780
+ }
781
+ /**
782
+ * Navigate to a URL
783
+ * @param input - Navigate input
784
+ * @param threadId - Optional thread ID for thread-safe operation
785
+ */
786
+ async navigate(input, threadId) {
787
+ const page = this.getPage(threadId);
788
+ if (!page) return this.createError("browser_error", "Browser page not available.", "Ensure the browser is launched.");
789
+ try {
790
+ await page.goto(input.url, { waitUntil: input.waitUntil ?? "domcontentloaded" });
791
+ return {
792
+ success: true,
793
+ url: page.url(),
794
+ title: await page.title(),
795
+ hint: "Page loaded. Use observe() to discover actions or extract() to get data."
796
+ };
797
+ } catch (error) {
798
+ return this.createErrorFromException(error, "Navigate");
799
+ }
800
+ }
801
+ /**
802
+ * Capture a screenshot of the current page
803
+ * @param input - Screenshot input
804
+ * @param threadId - Optional thread ID for thread-safe operation
805
+ */
806
+ async screenshot(input, threadId) {
807
+ const page = this.getPage(threadId);
808
+ if (!page) return this.createError("browser_error", "Browser page not available.", "Ensure the browser is launched.");
809
+ try {
810
+ const buffer = await page.screenshot({
811
+ fullPage: input.fullPage ?? false,
812
+ type: "png"
813
+ });
814
+ return {
815
+ base64: Buffer.from(buffer).toString("base64"),
816
+ url: page.url(),
817
+ title: await page.title()
818
+ };
819
+ } catch (error) {
820
+ return this.createErrorFromException(error, "Screenshot");
821
+ }
822
+ }
823
+ /**
824
+ * Manage browser tabs - list, create, switch, close
825
+ * @param input - Tabs input
826
+ * @param threadId - Optional thread ID for thread-safe operation
827
+ */
828
+ async tabs(input, threadId) {
829
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
830
+ const context = this.requireStagehand(effectiveThreadId).context;
831
+ if (!context) return this.createError("browser_error", "Browser context not available.", "Ensure the browser is launched.");
832
+ try {
833
+ switch (input.action) {
834
+ case "list": {
835
+ const pages = context.pages();
836
+ const activePage = context.activePage();
837
+ return {
838
+ success: true,
839
+ tabs: await Promise.all(pages.map(async (page, index) => ({
840
+ index,
841
+ url: page.url(),
842
+ title: await page.title(),
843
+ active: page === activePage
844
+ }))),
845
+ hint: "Use stagehand_tabs with action:\"switch\" and index to change tabs."
846
+ };
847
+ }
848
+ case "new": {
849
+ const newPage = await context.newPage(input.url);
850
+ await this.reconnectScreencastForThread(effectiveThreadId, "new tab via tool");
851
+ this.updateSessionBrowserState(effectiveThreadId);
852
+ return {
853
+ success: true,
854
+ index: context.pages().length - 1,
855
+ url: newPage.url(),
856
+ title: await newPage.title(),
857
+ hint: "New tab opened. Use stagehand_observe to discover actions."
858
+ };
859
+ }
860
+ case "switch": {
861
+ if (input.index === void 0) return this.createError("browser_error", "Tab index required for switch action.", "Provide index parameter.");
862
+ const pages = context.pages();
863
+ if (input.index < 0 || input.index >= pages.length) return this.createError("browser_error", `Invalid tab index: ${input.index}. Valid range: 0-${pages.length - 1}`, "Use stagehand_tabs with action:\"list\" to see available tabs.");
864
+ const targetPage = pages[input.index];
865
+ const targetUrl = targetPage.url();
866
+ context.setActivePage(targetPage);
867
+ await this.reconnectScreencastForThread(effectiveThreadId, "tab switch via tool");
868
+ const streamKey = this.getStreamKey(effectiveThreadId);
869
+ const stream = this.activeScreencastStreams.get(streamKey);
870
+ if (targetUrl && stream?.isActive()) stream.emitUrl(targetUrl);
871
+ this.updateSessionBrowserState(effectiveThreadId);
872
+ return {
873
+ success: true,
874
+ index: input.index,
875
+ url: targetUrl,
876
+ title: await targetPage.title(),
877
+ hint: "Tab switched. Use stagehand_observe to discover actions."
878
+ };
879
+ }
880
+ case "close": {
881
+ const pages = context.pages();
882
+ const indexToClose = input.index ?? pages.findIndex((p) => p === context.activePage());
883
+ if (indexToClose < 0 || indexToClose >= pages.length) return this.createError("browser_error", `Invalid tab index: ${indexToClose}`, "Use stagehand_tabs with action:\"list\" to see available tabs.");
884
+ await pages[indexToClose].close();
885
+ await this.reconnectScreencastForThread(effectiveThreadId, "tab close via tool");
886
+ this.updateSessionBrowserState(effectiveThreadId);
887
+ const remainingPages = context.pages();
888
+ return {
889
+ success: true,
890
+ remaining: remainingPages.length,
891
+ hint: remainingPages.length > 0 ? "Tab closed. Use stagehand_observe to see current tab." : "All tabs closed."
892
+ };
893
+ }
894
+ default: return this.createError("browser_error", `Unknown tabs action: ${input.action}`, "Use \"list\", \"new\", \"switch\", or \"close\".");
895
+ }
896
+ } catch (error) {
897
+ return this.createErrorFromException(error, "Tabs");
898
+ }
899
+ }
900
+ async getCurrentUrl(threadId) {
901
+ if (!this.isBrowserRunning()) return null;
902
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
903
+ if (this.threadManager.getScope() === "thread" && effectiveThreadId) {
904
+ const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
905
+ if (!stagehand?.context) return null;
906
+ const url = stagehand.context.activePage()?.url() ?? null;
907
+ if (url && url !== "about:blank") {
908
+ const state = this.getBrowserStateFromStagehand(stagehand);
909
+ if (state) this.threadManager.updateBrowserState(effectiveThreadId, state);
910
+ }
911
+ return url;
912
+ }
913
+ const page = this.getPage();
914
+ if (!page) return null;
915
+ try {
916
+ const url = page.url();
917
+ if (url && url !== "about:blank") {
918
+ const state = this.getBrowserStateFromStagehand(this.sharedManager);
919
+ if (state) this.lastBrowserState = state;
920
+ }
921
+ return url;
922
+ } catch {
923
+ return null;
924
+ }
925
+ }
926
+ /**
927
+ * Navigate to a URL (simple version). Used internally for restoring state on relaunch.
928
+ */
929
+ async navigateTo(url) {
930
+ const page = this.getPage();
931
+ if (!page) return;
932
+ try {
933
+ await page.goto(url, {
934
+ timeoutMs: this.config.timeout ?? 3e4,
935
+ waitUntil: "domcontentloaded"
936
+ });
937
+ } catch {}
938
+ }
939
+ /**
940
+ * Get the current browser state (all tabs and active tab index).
941
+ */
942
+ async getBrowserState(threadId) {
943
+ if (!this.isBrowserRunning()) return null;
944
+ try {
945
+ const scope = this.threadManager.getScope();
946
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
947
+ if (scope === "thread" && effectiveThreadId) {
948
+ const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
949
+ if (!stagehand) return null;
950
+ return this.getBrowserStateFromStagehand(stagehand);
951
+ }
952
+ return this.getBrowserStateFromStagehand(this.sharedManager);
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
957
+ /**
958
+ * Get browser state for a thread (implements abstract method from base class).
959
+ * Sync version that uses existing manager lookup without creating sessions.
960
+ */
961
+ getBrowserStateForThread(threadId) {
962
+ const effectiveThreadId = threadId ?? this.getCurrentThread() ?? _mastra_core_browser.DEFAULT_THREAD_ID;
963
+ const stagehand = this.threadManager.getExistingManagerForThread(effectiveThreadId);
964
+ return this.getBrowserStateFromStagehand(stagehand);
965
+ }
966
+ /**
967
+ * Get browser state from a specific Stagehand instance.
968
+ */
969
+ getBrowserStateFromStagehand(stagehand) {
970
+ if (!stagehand?.context) return null;
971
+ try {
972
+ const pages = stagehand.context.pages();
973
+ const activePage = stagehand.context.activePage();
974
+ let activeIndex = 0;
975
+ return {
976
+ tabs: pages.map((page, index) => {
977
+ if (page === activePage) activeIndex = index;
978
+ return { url: page.url() };
979
+ }),
980
+ activeTabIndex: activeIndex
981
+ };
982
+ } catch {
983
+ return null;
984
+ }
985
+ }
986
+ /**
987
+ * Get all open tabs with their URLs and titles.
988
+ */
989
+ async getTabState(threadId) {
990
+ return (await this.getBrowserState(threadId))?.tabs ?? [];
991
+ }
992
+ /**
993
+ * Get the active tab index.
994
+ */
995
+ async getActiveTabIndex(threadId) {
996
+ return (await this.getBrowserState(threadId))?.activeTabIndex ?? 0;
997
+ }
998
+ async startScreencast(options) {
999
+ const threadId = options?.threadId;
1000
+ const stream = new _mastra_core_browser.ScreencastStreamImpl({
1001
+ getCdpSession: async () => {
1002
+ const page = await this.threadManager.getPageForThread(threadId);
1003
+ if (!page) throw new Error("No page available for screencast");
1004
+ const session = this.getCdpSessionForPage(page);
1005
+ if (!session) throw new Error("No CDP session available for page");
1006
+ return session;
1007
+ },
1008
+ isBrowserRunning: () => this.isBrowserRunning()
1009
+ }, options);
1010
+ const streamKey = this.getStreamKey(threadId);
1011
+ this.activeScreencastStreams.set(streamKey, stream);
1012
+ await stream.start();
1013
+ await this.setupTabChangeDetection(threadId, stream);
1014
+ stream.once("stop", () => {
1015
+ if (this.activeScreencastStreams.get(streamKey) === stream) this.activeScreencastStreams.delete(streamKey);
1016
+ const timer = this.tabChangeDebounceTimers.get(streamKey);
1017
+ if (timer) {
1018
+ clearTimeout(timer);
1019
+ this.tabChangeDebounceTimers.delete(streamKey);
1020
+ }
1021
+ });
1022
+ return stream;
1023
+ }
1024
+ /**
1025
+ * Set up listeners to detect tab changes and reconnect the screencast.
1026
+ * Uses CDP Target events since Stagehand doesn't expose page lifecycle events.
1027
+ */
1028
+ async setupTabChangeDetection(threadId, stream) {
1029
+ const stagehand = await this.getManagerForThread(threadId);
1030
+ if (!stagehand?.context) return;
1031
+ const connection = stagehand.context.conn;
1032
+ if (!connection) {
1033
+ this.logger.debug?.("No CDP connection available for tab change detection");
1034
+ return;
1035
+ }
1036
+ const targetSessions = /* @__PURE__ */ new Map();
1037
+ let targetInfoDebounceTimer = null;
1038
+ const isTrackedByStagehand = (targetId) => {
1039
+ return (stagehand.context?.pages() || []).some((p) => p.targetId() === targetId);
1040
+ };
1041
+ const streamKey = this.getStreamKey(threadId);
1042
+ const onTargetCreated = (params) => {
1043
+ if (params.targetInfo.type !== "page") return;
1044
+ const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1045
+ if (existingTimer) clearTimeout(existingTimer);
1046
+ this.tabChangeDebounceTimers.set(streamKey, setTimeout(() => {
1047
+ this.tabChangeDebounceTimers.delete(streamKey);
1048
+ this.reconnectScreencastForThread(threadId, "new tab");
1049
+ setupPageNavigationListener();
1050
+ }, 300));
1051
+ };
1052
+ const onTargetAttached = (params) => {
1053
+ if (params.targetInfo.type !== "page") return;
1054
+ targetSessions.set(params.targetInfo.targetId, params.sessionId);
1055
+ };
1056
+ let pendingTargetInfo = null;
1057
+ const onTargetInfoChanged = (params) => {
1058
+ if (params.targetInfo.type !== "page") return;
1059
+ if (isTrackedByStagehand(params.targetInfo.targetId)) return;
1060
+ if (!targetSessions.get(params.targetInfo.targetId)) return;
1061
+ pendingTargetInfo = params;
1062
+ if (targetInfoDebounceTimer) clearTimeout(targetInfoDebounceTimer);
1063
+ targetInfoDebounceTimer = setTimeout(async () => {
1064
+ targetInfoDebounceTimer = null;
1065
+ if (!pendingTargetInfo) return;
1066
+ const info = pendingTargetInfo.targetInfo;
1067
+ const sid = targetSessions.get(info.targetId);
1068
+ pendingTargetInfo = null;
1069
+ if (isTrackedByStagehand(info.targetId) || !sid) return;
1070
+ const contextAny = stagehand.context;
1071
+ if (contextAny?.onAttachedToTarget) try {
1072
+ await contextAny.onAttachedToTarget(info, sid);
1073
+ await new Promise((resolve) => setTimeout(resolve, 100));
1074
+ if (isTrackedByStagehand(info.targetId)) {
1075
+ this.logger.debug?.("Page registered successfully, setting as active");
1076
+ const newPage = (stagehand.context?.pages() || []).find((p) => p.targetId() === info.targetId);
1077
+ if (newPage && stagehand.context) stagehand.context.setActivePage(newPage);
1078
+ this.reconnectScreencastForThread(threadId, "manual tab tracked");
1079
+ setupPageNavigationListener();
1080
+ } else this.logger.debug?.("Stagehand did not register the page (non-injectable URL)");
1081
+ } catch (e) {
1082
+ this.logger.debug?.("Failed to register page with Stagehand", e);
1083
+ }
1084
+ }, 300);
1085
+ };
1086
+ const onTargetDestroyed = (params) => {
1087
+ this.logger.debug?.("Page target destroyed");
1088
+ targetSessions.delete(params.targetId);
1089
+ const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1090
+ if (existingTimer) clearTimeout(existingTimer);
1091
+ this.tabChangeDebounceTimers.set(streamKey, setTimeout(() => {
1092
+ this.tabChangeDebounceTimers.delete(streamKey);
1093
+ this.reconnectScreencastForThread(threadId, "tab closed");
1094
+ setupPageNavigationListener();
1095
+ }, 300));
1096
+ };
1097
+ const onFrameNavigated = (params) => {
1098
+ if (!params.frame.parentId && params.frame.url) {
1099
+ stream.emitUrl(params.frame.url);
1100
+ this.updateSessionBrowserState(threadId);
1101
+ const existingTimer = this.tabChangeDebounceTimers.get(streamKey);
1102
+ if (existingTimer) clearTimeout(existingTimer);
1103
+ this.tabChangeDebounceTimers.set(streamKey, setTimeout(() => {
1104
+ this.tabChangeDebounceTimers.delete(streamKey);
1105
+ this.reconnectScreencastForThread(threadId, "same-tab navigation");
1106
+ }, 300));
1107
+ }
1108
+ };
1109
+ let pageSession = null;
1110
+ const setupPageNavigationListener = async () => {
1111
+ try {
1112
+ if (pageSession?.off) pageSession.off("Page.frameNavigated", onFrameNavigated);
1113
+ const page = stagehand.context?.activePage();
1114
+ if (!page) return;
1115
+ const session = page.getSessionForFrame(page.mainFrameId());
1116
+ if (!session) return;
1117
+ pageSession = session;
1118
+ await session.send("Page.enable");
1119
+ session.on("Page.frameNavigated", onFrameNavigated);
1120
+ const currentUrl = page.url();
1121
+ if (currentUrl && currentUrl !== "about:blank") stream.emitUrl(currentUrl);
1122
+ } catch (error) {
1123
+ this.logger.debug?.("Failed to set up page navigation listener", error);
1124
+ }
1125
+ };
1126
+ const cleanup = () => {
1127
+ const timer = this.tabChangeDebounceTimers.get(streamKey);
1128
+ if (timer) {
1129
+ clearTimeout(timer);
1130
+ this.tabChangeDebounceTimers.delete(streamKey);
1131
+ }
1132
+ if (targetInfoDebounceTimer) {
1133
+ clearTimeout(targetInfoDebounceTimer);
1134
+ targetInfoDebounceTimer = null;
1135
+ }
1136
+ connection.off?.("Target.targetCreated", onTargetCreated);
1137
+ connection.off?.("Target.targetDestroyed", onTargetDestroyed);
1138
+ connection.off?.("Target.attachedToTarget", onTargetAttached);
1139
+ connection.off?.("Target.targetInfoChanged", onTargetInfoChanged);
1140
+ if (pageSession?.off) pageSession.off("Page.frameNavigated", onFrameNavigated);
1141
+ };
1142
+ stream.once("stop", cleanup);
1143
+ try {
1144
+ connection.on?.("Target.targetCreated", onTargetCreated);
1145
+ connection.on?.("Target.targetDestroyed", onTargetDestroyed);
1146
+ connection.on?.("Target.attachedToTarget", onTargetAttached);
1147
+ connection.on?.("Target.targetInfoChanged", onTargetInfoChanged);
1148
+ await setupPageNavigationListener();
1149
+ } catch (error) {
1150
+ this.logger.debug?.("Failed to set up tab change detection", error);
1151
+ }
1152
+ }
1153
+ async injectMouseEvent(event, threadId) {
1154
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1155
+ const page = await this.threadManager.getPageForThread(effectiveThreadId);
1156
+ const cdpSession = this.getCdpSessionForPage(page);
1157
+ if (!cdpSession) throw new Error("No CDP session available");
1158
+ const buttonMap = {
1159
+ none: 0,
1160
+ left: 1,
1161
+ middle: 4,
1162
+ right: 2
1163
+ };
1164
+ const defaultClickCount = event.type === "mousePressed" || event.type === "mouseReleased" ? 1 : 0;
1165
+ await cdpSession.send("Input.dispatchMouseEvent", {
1166
+ type: event.type,
1167
+ x: event.x,
1168
+ y: event.y,
1169
+ button: event.button ?? "none",
1170
+ buttons: buttonMap[event.button ?? "none"] ?? 0,
1171
+ clickCount: event.clickCount ?? defaultClickCount,
1172
+ deltaX: event.deltaX ?? 0,
1173
+ deltaY: event.deltaY ?? 0,
1174
+ modifiers: event.modifiers ?? 0
1175
+ });
1176
+ }
1177
+ async injectKeyboardEvent(event, threadId) {
1178
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1179
+ const page = await this.threadManager.getPageForThread(effectiveThreadId);
1180
+ const cdpSession = this.getCdpSessionForPage(page);
1181
+ if (!cdpSession) throw new Error("No CDP session available");
1182
+ await cdpSession.send("Input.dispatchKeyEvent", {
1183
+ type: event.type,
1184
+ key: event.key,
1185
+ code: event.code,
1186
+ text: event.text,
1187
+ modifiers: event.modifiers ?? 0,
1188
+ windowsVirtualKeyCode: event.windowsVirtualKeyCode
1189
+ });
1190
+ }
1308
1191
  };
1309
-
1192
+ //#endregion
1193
+ //#region src/types.ts
1194
+ /**
1195
+ * Providers Stagehand can resolve from a `provider/model` string.
1196
+ *
1197
+ * Stagehand splits the model id on its first slash and looks the prefix up in
1198
+ * its internal AI SDK provider registry; an unknown prefix throws during
1199
+ * browser startup rather than at configuration time. Mirrored here so callers
1200
+ * can reject a bad provider up front. Keep in sync with `AISDKProviders` in
1201
+ * `@browserbasehq/stagehand`.
1202
+ */
1203
+ const STAGEHAND_MODEL_PROVIDERS = [
1204
+ "anthropic",
1205
+ "azure",
1206
+ "bedrock",
1207
+ "cerebras",
1208
+ "deepseek",
1209
+ "gateway",
1210
+ "google",
1211
+ "groq",
1212
+ "mistral",
1213
+ "ollama",
1214
+ "openai",
1215
+ "perplexity",
1216
+ "togetherai",
1217
+ "vertex",
1218
+ "xai"
1219
+ ];
1220
+ //#endregion
1221
+ exports.STAGEHAND_MODEL_PROVIDERS = STAGEHAND_MODEL_PROVIDERS;
1310
1222
  exports.STAGEHAND_TOOLS = STAGEHAND_TOOLS;
1311
1223
  exports.StagehandBrowser = StagehandBrowser;
1312
1224
  exports.actInputSchema = actInputSchema;
@@ -1318,5 +1230,5 @@ exports.navigateInputSchema = navigateInputSchema;
1318
1230
  exports.observeInputSchema = observeInputSchema;
1319
1231
  exports.stagehandSchemas = stagehandSchemas;
1320
1232
  exports.tabsInputSchema = tabsInputSchema;
1321
- //# sourceMappingURL=index.cjs.map
1233
+
1322
1234
  //# sourceMappingURL=index.cjs.map