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