@mastra/agent-browser 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,1781 +1,1657 @@
1
- 'use strict';
2
-
3
- var browser = require('@mastra/core/browser');
4
- var agentBrowser = require('agent-browser');
5
- var tools = require('@mastra/core/tools');
6
- var zod = require('zod');
7
-
8
- // src/agent-browser.ts
9
- var AgentBrowserThreadManager = class extends browser.ThreadManager {
10
- browserConfig;
11
- resolveCdpUrl;
12
- onBrowserCreated;
13
- constructor(config) {
14
- super(config);
15
- this.browserConfig = config.browserConfig;
16
- this.resolveCdpUrl = config.resolveCdpUrl;
17
- this.onBrowserCreated = config.onBrowserCreated;
18
- }
19
- /**
20
- * Get the page for a specific thread, creating session if needed.
21
- */
22
- async getPageForThread(threadId) {
23
- const manager = await this.getManagerForThread(threadId);
24
- return manager.getPage();
25
- }
26
- /**
27
- * Create a new session for a thread.
28
- */
29
- async createSession(threadId) {
30
- const savedState = this.getSavedBrowserState(threadId);
31
- const session = {
32
- threadId,
33
- createdAt: Date.now(),
34
- browserState: savedState
35
- };
36
- if (this.scope === "thread") {
37
- const manager = new agentBrowser.BrowserManager();
38
- const launchOptions = {
39
- headless: this.browserConfig.headless,
40
- viewport: this.browserConfig.viewport,
41
- profile: this.browserConfig.profile,
42
- executablePath: this.browserConfig.executablePath,
43
- storageState: this.browserConfig.storageState
44
- };
45
- if (this.browserConfig.cdpUrl && this.resolveCdpUrl) {
46
- launchOptions.cdpUrl = await this.resolveCdpUrl(this.browserConfig.cdpUrl);
47
- }
48
- try {
49
- await manager.launch(launchOptions);
50
- } catch (error) {
51
- try {
52
- await manager.close();
53
- } catch {
54
- }
55
- throw error;
56
- }
57
- session.manager = manager;
58
- this.threadManagers.set(threadId, manager);
59
- try {
60
- if (savedState && savedState.tabs.length > 0) {
61
- this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
62
- await this.restoreBrowserState(manager, savedState);
63
- }
64
- this.onBrowserCreated?.(manager, threadId);
65
- } catch (error) {
66
- this.threadManagers.delete(threadId);
67
- session.manager = void 0;
68
- try {
69
- await manager.close();
70
- } catch {
71
- }
72
- throw error;
73
- }
74
- }
75
- return session;
76
- }
77
- /**
78
- * Restore browser state (multiple tabs) to a browser manager.
79
- */
80
- async restoreBrowserState(manager, state) {
81
- try {
82
- const firstTab = state.tabs[0];
83
- if (firstTab?.url) {
84
- const page = manager.getPage();
85
- if (page) {
86
- await page.goto(firstTab.url, { waitUntil: "domcontentloaded" });
87
- }
88
- }
89
- for (let i = 1; i < state.tabs.length; i++) {
90
- const tab = state.tabs[i];
91
- if (tab?.url) {
92
- await manager.newTab();
93
- const page = manager.getPage();
94
- if (page) {
95
- await page.goto(tab.url, { waitUntil: "domcontentloaded" });
96
- }
97
- }
98
- }
99
- if (state.tabs.length > 1 && state.activeTabIndex >= 0 && state.activeTabIndex < state.tabs.length) {
100
- await manager.switchTo(state.activeTabIndex);
101
- }
102
- } catch (error) {
103
- this.logger?.warn?.(`Failed to restore browser state: ${error}`);
104
- }
105
- }
106
- /**
107
- * Get the browser manager for a specific session.
108
- */
109
- getManagerForSession(session) {
110
- if (this.scope === "thread" && session.manager) {
111
- return session.manager;
112
- }
113
- return this.getSharedManager();
114
- }
115
- /**
116
- * Destroy a session and clean up resources.
117
- */
118
- async doDestroySession(session) {
119
- if (this.scope === "thread" && session.manager) {
120
- await session.manager.close();
121
- }
122
- }
123
- /**
124
- * Destroy all sessions (called during browser close).
125
- * doDestroySession handles closing individual browser managers.
126
- */
127
- async destroyAllSessions() {
128
- await super.destroyAllSessions();
129
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_browser = require("@mastra/core/browser");
3
+ let agent_browser = require("agent-browser");
4
+ let _mastra_core_tools = require("@mastra/core/tools");
5
+ let zod = require("zod");
6
+ //#region src/thread-manager.ts
7
+ /**
8
+ * AgentBrowserThreadManager - Thread scope management for AgentBrowser
9
+ *
10
+ * Manages thread-scoped browser sessions using agent-browser's
11
+ * BrowserManager capabilities (newWindow, switchTo, closeTab).
12
+ */
13
+ /**
14
+ * Thread manager implementation for AgentBrowser.
15
+ *
16
+ * Supports two scope modes:
17
+ * - 'shared': All threads share the shared browser manager
18
+ * - 'thread': Each thread gets a dedicated browser manager instance
19
+ */
20
+ var AgentBrowserThreadManager = class extends _mastra_core_browser.ThreadManager {
21
+ browserConfig;
22
+ resolveCdpUrl;
23
+ onBrowserCreated;
24
+ constructor(config) {
25
+ super(config);
26
+ this.browserConfig = config.browserConfig;
27
+ this.resolveCdpUrl = config.resolveCdpUrl;
28
+ this.onBrowserCreated = config.onBrowserCreated;
29
+ }
30
+ /**
31
+ * Get the page for a specific thread, creating session if needed.
32
+ */
33
+ async getPageForThread(threadId) {
34
+ return (await this.getManagerForThread(threadId)).getPage();
35
+ }
36
+ /**
37
+ * Create a new session for a thread.
38
+ */
39
+ async createSession(threadId) {
40
+ const savedState = this.getSavedBrowserState(threadId);
41
+ const session = {
42
+ threadId,
43
+ createdAt: Date.now(),
44
+ browserState: savedState
45
+ };
46
+ if (this.scope === "thread") {
47
+ const manager = new agent_browser.BrowserManager();
48
+ const launchOptions = {
49
+ headless: this.browserConfig.headless,
50
+ viewport: this.browserConfig.viewport,
51
+ profile: this.browserConfig.profile,
52
+ executablePath: this.browserConfig.executablePath,
53
+ storageState: this.browserConfig.storageState
54
+ };
55
+ if (this.browserConfig.cdpUrl && this.resolveCdpUrl) launchOptions.cdpUrl = await this.resolveCdpUrl(this.browserConfig.cdpUrl);
56
+ if (this.browserConfig.cdpHeaders) launchOptions.cdpHeaders = this.browserConfig.cdpHeaders;
57
+ try {
58
+ await manager.launch(launchOptions);
59
+ } catch (error) {
60
+ try {
61
+ await manager.close();
62
+ } catch {}
63
+ throw error;
64
+ }
65
+ session.manager = manager;
66
+ this.threadManagers.set(threadId, manager);
67
+ try {
68
+ if (savedState && savedState.tabs.length > 0) {
69
+ this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
70
+ await this.restoreBrowserState(manager, savedState);
71
+ }
72
+ this.onBrowserCreated?.(manager, threadId);
73
+ } catch (error) {
74
+ this.threadManagers.delete(threadId);
75
+ session.manager = void 0;
76
+ try {
77
+ await manager.close();
78
+ } catch {}
79
+ throw error;
80
+ }
81
+ }
82
+ return session;
83
+ }
84
+ /**
85
+ * Restore browser state (multiple tabs) to a browser manager.
86
+ */
87
+ async restoreBrowserState(manager, state) {
88
+ try {
89
+ const firstTab = state.tabs[0];
90
+ if (firstTab?.url) {
91
+ const page = manager.getPage();
92
+ if (page) await page.goto(firstTab.url, { waitUntil: "domcontentloaded" });
93
+ }
94
+ for (let i = 1; i < state.tabs.length; i++) {
95
+ const tab = state.tabs[i];
96
+ if (tab?.url) {
97
+ await manager.newTab();
98
+ const page = manager.getPage();
99
+ if (page) await page.goto(tab.url, { waitUntil: "domcontentloaded" });
100
+ }
101
+ }
102
+ if (state.tabs.length > 1 && state.activeTabIndex >= 0 && state.activeTabIndex < state.tabs.length) await manager.switchTo(state.activeTabIndex);
103
+ } catch (error) {
104
+ this.logger?.warn?.(`Failed to restore browser state: ${error}`);
105
+ }
106
+ }
107
+ /**
108
+ * Get the browser manager for a specific session.
109
+ */
110
+ getManagerForSession(session) {
111
+ if (this.scope === "thread" && session.manager) return session.manager;
112
+ return this.getSharedManager();
113
+ }
114
+ /**
115
+ * Destroy a session and clean up resources.
116
+ */
117
+ async doDestroySession(session) {
118
+ if (this.scope === "thread" && session.manager) await session.manager.close();
119
+ }
120
+ /**
121
+ * Destroy all sessions (called during browser close).
122
+ * doDestroySession handles closing individual browser managers.
123
+ */
124
+ async destroyAllSessions() {
125
+ await super.destroyAllSessions();
126
+ }
130
127
  };
131
- var gotoInputSchema = zod.z.object({
132
- url: zod.z.string().describe("The URL to navigate to"),
133
- waitUntil: zod.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("When to consider navigation complete (default: domcontentloaded)"),
134
- timeout: zod.z.number().optional().describe("Navigation timeout in milliseconds")
128
+ //#endregion
129
+ //#region src/schemas.ts
130
+ /**
131
+ * AgentBrowser Tool Schemas
132
+ *
133
+ * Flat schemas for browser tools. Each tool has a single-purpose schema
134
+ * without discriminated unions, making them easier for LLMs to understand.
135
+ *
136
+ * Tools:
137
+ * - Core: goto, snapshot, click, type, press, select, scroll, close
138
+ * - Extended: hover, back, dialog, wait, tabs, drag
139
+ * - Escape Hatch: evaluate
140
+ */
141
+ /**
142
+ * browser_goto - Navigate to a URL
143
+ */
144
+ const gotoInputSchema = zod.z.object({
145
+ url: zod.z.string().describe("The URL to navigate to"),
146
+ waitUntil: zod.z.enum([
147
+ "load",
148
+ "domcontentloaded",
149
+ "networkidle"
150
+ ]).optional().describe("When to consider navigation complete (default: domcontentloaded)"),
151
+ timeout: zod.z.number().optional().describe("Navigation timeout in milliseconds")
135
152
  });
136
- var snapshotInputSchema = zod.z.object({
137
- interactiveOnly: zod.z.boolean().optional().describe("Only include interactive elements (default: true)"),
138
- maxDepth: zod.z.number().optional().describe("Maximum depth of the tree to return")
153
+ /**
154
+ * browser_snapshot - Get accessibility tree snapshot
155
+ */
156
+ const snapshotInputSchema = zod.z.object({
157
+ interactiveOnly: zod.z.boolean().optional().describe("Only include interactive elements (default: true)"),
158
+ maxDepth: zod.z.number().optional().describe("Maximum depth of the tree to return")
139
159
  });
140
- var clickInputSchema = zod.z.object({
141
- ref: zod.z.string().describe("Element ref from snapshot (e.g., @e5)"),
142
- button: zod.z.enum(["left", "right", "middle"]).optional().describe("Mouse button (default: left)"),
143
- clickCount: zod.z.number().optional().describe("Number of clicks (default: 1, use 2 for double-click)"),
144
- modifiers: zod.z.array(zod.z.enum(["Alt", "Control", "Meta", "Shift"])).optional().describe("Modifier keys to hold"),
145
- waitUntil: zod.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("If the click triggers a navigation, wait for this page load state before returning"),
146
- timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the click and optional waitUntil")
160
+ /**
161
+ * browser_click - Click an element
162
+ */
163
+ const clickInputSchema = zod.z.object({
164
+ ref: zod.z.string().describe("Element ref from snapshot (e.g., @e5)"),
165
+ button: zod.z.enum([
166
+ "left",
167
+ "right",
168
+ "middle"
169
+ ]).optional().describe("Mouse button (default: left)"),
170
+ clickCount: zod.z.number().optional().describe("Number of clicks (default: 1, use 2 for double-click)"),
171
+ modifiers: zod.z.array(zod.z.enum([
172
+ "Alt",
173
+ "Control",
174
+ "Meta",
175
+ "Shift"
176
+ ])).optional().describe("Modifier keys to hold"),
177
+ waitUntil: zod.z.enum([
178
+ "load",
179
+ "domcontentloaded",
180
+ "networkidle"
181
+ ]).optional().describe("If the click triggers a navigation, wait for this page load state before returning"),
182
+ timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the click and optional waitUntil")
147
183
  });
148
- var typeInputSchema = zod.z.object({
149
- ref: zod.z.string().describe("Element ref from snapshot"),
150
- text: zod.z.string().describe("Text to type"),
151
- clear: zod.z.boolean().optional().describe("Clear existing content before typing (default: false)"),
152
- delay: zod.z.number().optional().describe("Delay between keystrokes in ms")
184
+ /**
185
+ * browser_type - Type text into an element
186
+ */
187
+ const typeInputSchema = zod.z.object({
188
+ ref: zod.z.string().describe("Element ref from snapshot"),
189
+ text: zod.z.string().describe("Text to type"),
190
+ clear: zod.z.boolean().optional().describe("Clear existing content before typing (default: false)"),
191
+ delay: zod.z.number().optional().describe("Delay between keystrokes in ms")
153
192
  });
154
- var pressInputSchema = zod.z.object({
155
- key: zod.z.string().describe("Key to press (e.g., Enter, Tab, Escape, Control+a)"),
156
- modifiers: zod.z.array(zod.z.enum(["Alt", "Control", "Meta", "Shift"])).optional().describe("Modifier keys to hold"),
157
- waitUntil: zod.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("If the key press triggers a navigation, wait for this page load state before returning"),
158
- timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the optional waitUntil")
193
+ /**
194
+ * browser_press - Press a keyboard key
195
+ */
196
+ const pressInputSchema = zod.z.object({
197
+ key: zod.z.string().describe("Key to press (e.g., Enter, Tab, Escape, Control+a)"),
198
+ modifiers: zod.z.array(zod.z.enum([
199
+ "Alt",
200
+ "Control",
201
+ "Meta",
202
+ "Shift"
203
+ ])).optional().describe("Modifier keys to hold"),
204
+ waitUntil: zod.z.enum([
205
+ "load",
206
+ "domcontentloaded",
207
+ "networkidle"
208
+ ]).optional().describe("If the key press triggers a navigation, wait for this page load state before returning"),
209
+ timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the optional waitUntil")
159
210
  });
160
- var selectInputSchema = zod.z.object({
161
- ref: zod.z.string().describe("Select element ref from snapshot"),
162
- value: zod.z.string().optional().describe("Option value to select"),
163
- label: zod.z.string().optional().describe("Option label to select"),
164
- index: zod.z.number().int().min(0).optional().describe("Option index to select (0-based)"),
165
- waitUntil: zod.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("If the selection triggers a navigation, wait for this page load state before returning"),
166
- timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the selection and optional waitUntil")
211
+ /**
212
+ * browser_select - Select option from dropdown
213
+ */
214
+ const selectInputSchema = zod.z.object({
215
+ ref: zod.z.string().describe("Select element ref from snapshot"),
216
+ value: zod.z.string().optional().describe("Option value to select"),
217
+ label: zod.z.string().optional().describe("Option label to select"),
218
+ index: zod.z.number().int().min(0).optional().describe("Option index to select (0-based)"),
219
+ waitUntil: zod.z.enum([
220
+ "load",
221
+ "domcontentloaded",
222
+ "networkidle"
223
+ ]).optional().describe("If the selection triggers a navigation, wait for this page load state before returning"),
224
+ timeout: zod.z.number().nonnegative().optional().describe("Timeout in milliseconds for the selection and optional waitUntil")
167
225
  }).superRefine((data, ctx) => {
168
- if (data.value === void 0 && data.label === void 0 && data.index === void 0) {
169
- ctx.addIssue({
170
- code: zod.z.ZodIssueCode.custom,
171
- message: "At least one of value, label, or index is required"
172
- });
173
- }
174
- });
175
- var scrollInputSchema = zod.z.object({
176
- direction: zod.z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
177
- amount: zod.z.number().optional().describe("Scroll amount in pixels (default: 300)"),
178
- ref: zod.z.string().optional().describe("Element ref to scroll (scrolls page if omitted)")
226
+ if (data.value === void 0 && data.label === void 0 && data.index === void 0) ctx.addIssue({
227
+ code: zod.z.ZodIssueCode.custom,
228
+ message: "At least one of value, label, or index is required"
229
+ });
179
230
  });
180
- var closeInputSchema = zod.z.object({});
181
- var hoverInputSchema = zod.z.object({
182
- ref: zod.z.string().describe("Element ref from snapshot")
231
+ /**
232
+ * browser_scroll - Scroll the page or element
233
+ */
234
+ const scrollInputSchema = zod.z.object({
235
+ direction: zod.z.enum([
236
+ "up",
237
+ "down",
238
+ "left",
239
+ "right"
240
+ ]).describe("Scroll direction"),
241
+ amount: zod.z.number().optional().describe("Scroll amount in pixels (default: 300)"),
242
+ ref: zod.z.string().optional().describe("Element ref to scroll (scrolls page if omitted)")
183
243
  });
184
- var backInputSchema = zod.z.object({});
185
- var dialogInputSchema = zod.z.object({
186
- triggerRef: zod.z.string().describe("Element ref that triggers the dialog (e.g., @e5)"),
187
- action: zod.z.enum(["accept", "dismiss"]).describe("Accept or dismiss the dialog"),
188
- text: zod.z.string().optional().describe("Text to enter for prompt dialogs")
244
+ /**
245
+ * browser_close - Close the browser
246
+ */
247
+ const closeInputSchema = zod.z.object({});
248
+ /**
249
+ * browser_hover - Hover over an element
250
+ */
251
+ const hoverInputSchema = zod.z.object({ ref: zod.z.string().describe("Element ref from snapshot") });
252
+ /**
253
+ * browser_back - Go back in browser history
254
+ */
255
+ const backInputSchema = zod.z.object({});
256
+ /**
257
+ * browser_dialog - Click an element that triggers a dialog and handle it
258
+ */
259
+ const dialogInputSchema = zod.z.object({
260
+ triggerRef: zod.z.string().describe("Element ref that triggers the dialog (e.g., @e5)"),
261
+ action: zod.z.enum(["accept", "dismiss"]).describe("Accept or dismiss the dialog"),
262
+ text: zod.z.string().optional().describe("Text to enter for prompt dialogs")
189
263
  });
190
- var waitInputSchema = zod.z.object({
191
- ref: zod.z.string().optional().describe("Element ref to wait for"),
192
- state: zod.z.enum(["visible", "hidden", "attached", "detached"]).optional().describe("State to wait for (default: visible)"),
193
- timeout: zod.z.number().optional().describe("Maximum wait time in ms (default: 30000)")
264
+ /**
265
+ * browser_wait - Wait for an element or condition
266
+ */
267
+ const waitInputSchema = zod.z.object({
268
+ ref: zod.z.string().optional().describe("Element ref to wait for"),
269
+ state: zod.z.enum([
270
+ "visible",
271
+ "hidden",
272
+ "attached",
273
+ "detached"
274
+ ]).optional().describe("State to wait for (default: visible)"),
275
+ timeout: zod.z.number().optional().describe("Maximum wait time in ms (default: 30000)")
194
276
  });
195
- var tabsInputSchema = zod.z.object({
196
- action: zod.z.enum(["list", "new", "switch", "close"]).describe("Tab action"),
197
- index: zod.z.number().int().min(0).optional().describe("Tab index for switch/close"),
198
- url: zod.z.string().optional().describe("URL to open in new tab")
277
+ /**
278
+ * browser_tabs - Manage browser tabs
279
+ */
280
+ const tabsInputSchema = zod.z.object({
281
+ action: zod.z.enum([
282
+ "list",
283
+ "new",
284
+ "switch",
285
+ "close"
286
+ ]).describe("Tab action"),
287
+ index: zod.z.number().int().min(0).optional().describe("Tab index for switch/close"),
288
+ url: zod.z.string().optional().describe("URL to open in new tab")
199
289
  }).superRefine((value, ctx) => {
200
- if (value.action === "switch" && value.index === void 0) {
201
- ctx.addIssue({
202
- code: zod.z.ZodIssueCode.custom,
203
- path: ["index"],
204
- message: 'index is required when action is "switch"'
205
- });
206
- }
290
+ if (value.action === "switch" && value.index === void 0) ctx.addIssue({
291
+ code: zod.z.ZodIssueCode.custom,
292
+ path: ["index"],
293
+ message: "index is required when action is \"switch\""
294
+ });
207
295
  });
208
- var dragInputSchema = zod.z.object({
209
- sourceRef: zod.z.string().optional().describe("Element ref to drag from (e.g., @e5)"),
210
- targetRef: zod.z.string().optional().describe("Element ref to drag to (e.g., @e7)"),
211
- sourceSelector: zod.z.string().optional().describe("CSS selector for source element (use if ref not available)"),
212
- targetSelector: zod.z.string().optional().describe("CSS selector for target element (use if ref not available)")
296
+ /**
297
+ * browser_drag - Drag an element to another element
298
+ */
299
+ const dragInputSchema = zod.z.object({
300
+ sourceRef: zod.z.string().optional().describe("Element ref to drag from (e.g., @e5)"),
301
+ targetRef: zod.z.string().optional().describe("Element ref to drag to (e.g., @e7)"),
302
+ sourceSelector: zod.z.string().optional().describe("CSS selector for source element (use if ref not available)"),
303
+ targetSelector: zod.z.string().optional().describe("CSS selector for target element (use if ref not available)")
213
304
  }).superRefine((data, ctx) => {
214
- if (!data.sourceRef && !data.sourceSelector) {
215
- ctx.addIssue({
216
- code: zod.z.ZodIssueCode.custom,
217
- path: ["sourceRef"],
218
- message: "Either sourceRef or sourceSelector is required"
219
- });
220
- }
221
- if (!data.targetRef && !data.targetSelector) {
222
- ctx.addIssue({
223
- code: zod.z.ZodIssueCode.custom,
224
- path: ["targetRef"],
225
- message: "Either targetRef or targetSelector is required"
226
- });
227
- }
305
+ if (!data.sourceRef && !data.sourceSelector) ctx.addIssue({
306
+ code: zod.z.ZodIssueCode.custom,
307
+ path: ["sourceRef"],
308
+ message: "Either sourceRef or sourceSelector is required"
309
+ });
310
+ if (!data.targetRef && !data.targetSelector) ctx.addIssue({
311
+ code: zod.z.ZodIssueCode.custom,
312
+ path: ["targetRef"],
313
+ message: "Either targetRef or targetSelector is required"
314
+ });
228
315
  });
229
- var screenshotInputSchema = zod.z.object({
230
- fullPage: zod.z.boolean().optional().describe("Capture the full scrollable page instead of just the viewport (default: false)")
316
+ /**
317
+ * browser_screenshot - Capture a screenshot of the current page
318
+ */
319
+ const screenshotInputSchema = zod.z.object({ fullPage: zod.z.boolean().optional().describe("Capture the full scrollable page instead of just the viewport (default: false)") });
320
+ /**
321
+ * browser_evaluate - Execute JavaScript in the browser
322
+ */
323
+ const evaluateInputSchema = zod.z.object({
324
+ script: zod.z.string().describe("JavaScript expression to evaluate in the browser and return the result. Do not use `return` — write a bare expression like `document.title` or `1 + 1`. For async code, wrap in an async IIFE: `(async () => { ... })()`."),
325
+ arg: zod.z.unknown().optional().describe("Argument to pass to the script (JSON-serializable)")
231
326
  });
232
- var evaluateInputSchema = zod.z.object({
233
- script: zod.z.string().describe(
234
- "JavaScript expression to evaluate in the browser and return the result. Do not use `return` \u2014 write a bare expression like `document.title` or `1 + 1`. For async code, wrap in an async IIFE: `(async () => { ... })()`."
235
- ),
236
- arg: zod.z.unknown().optional().describe("Argument to pass to the script (JSON-serializable)")
237
- });
238
- var browserSchemas = {
239
- // Core
240
- goto: gotoInputSchema,
241
- snapshot: snapshotInputSchema,
242
- click: clickInputSchema,
243
- type: typeInputSchema,
244
- press: pressInputSchema,
245
- select: selectInputSchema,
246
- scroll: scrollInputSchema,
247
- close: closeInputSchema,
248
- // Extended
249
- hover: hoverInputSchema,
250
- back: backInputSchema,
251
- dialog: dialogInputSchema,
252
- wait: waitInputSchema,
253
- tabs: tabsInputSchema,
254
- drag: dragInputSchema,
255
- // Utility
256
- screenshot: screenshotInputSchema,
257
- // Escape hatch
258
- evaluate: evaluateInputSchema
327
+ const browserSchemas = {
328
+ goto: gotoInputSchema,
329
+ snapshot: snapshotInputSchema,
330
+ click: clickInputSchema,
331
+ type: typeInputSchema,
332
+ press: pressInputSchema,
333
+ select: selectInputSchema,
334
+ scroll: scrollInputSchema,
335
+ close: closeInputSchema,
336
+ hover: hoverInputSchema,
337
+ back: backInputSchema,
338
+ dialog: dialogInputSchema,
339
+ wait: waitInputSchema,
340
+ tabs: tabsInputSchema,
341
+ drag: dragInputSchema,
342
+ screenshot: screenshotInputSchema,
343
+ evaluate: evaluateInputSchema
259
344
  };
260
-
261
- // src/tools/constants.ts
262
- var BROWSER_TOOLS = {
263
- // Core
264
- GOTO: "browser_goto",
265
- SNAPSHOT: "browser_snapshot",
266
- CLICK: "browser_click",
267
- TYPE: "browser_type",
268
- PRESS: "browser_press",
269
- SELECT: "browser_select",
270
- SCROLL: "browser_scroll",
271
- CLOSE: "browser_close",
272
- // Extended
273
- HOVER: "browser_hover",
274
- BACK: "browser_back",
275
- DIALOG: "browser_dialog",
276
- WAIT: "browser_wait",
277
- TABS: "browser_tabs",
278
- DRAG: "browser_drag",
279
- // Utility
280
- SCREENSHOT: "browser_screenshot",
281
- // Escape hatch
282
- EVALUATE: "browser_evaluate"
345
+ //#endregion
346
+ //#region src/tools/constants.ts
347
+ /**
348
+ * Browser Tool Constants
349
+ */
350
+ const BROWSER_TOOLS = {
351
+ GOTO: "browser_goto",
352
+ SNAPSHOT: "browser_snapshot",
353
+ CLICK: "browser_click",
354
+ TYPE: "browser_type",
355
+ PRESS: "browser_press",
356
+ SELECT: "browser_select",
357
+ SCROLL: "browser_scroll",
358
+ CLOSE: "browser_close",
359
+ HOVER: "browser_hover",
360
+ BACK: "browser_back",
361
+ DIALOG: "browser_dialog",
362
+ WAIT: "browser_wait",
363
+ TABS: "browser_tabs",
364
+ DRAG: "browser_drag",
365
+ SCREENSHOT: "browser_screenshot",
366
+ EVALUATE: "browser_evaluate"
283
367
  };
284
-
285
- // src/tools/back.ts
368
+ //#endregion
369
+ //#region src/tools/back.ts
370
+ /**
371
+ * browser_back - Go back in browser history
372
+ */
286
373
  function createBackTool(browser) {
287
- return tools.createTool({
288
- id: BROWSER_TOOLS.BACK,
289
- description: "Go back to the previous page in browser history.",
290
- inputSchema: backInputSchema,
291
- execute: async (_input, { agent }) => {
292
- const threadId = agent?.threadId;
293
- browser.setCurrentThread(threadId);
294
- await browser.ensureReady();
295
- return browser.back(threadId);
296
- }
297
- });
374
+ return (0, _mastra_core_tools.createTool)({
375
+ id: BROWSER_TOOLS.BACK,
376
+ description: "Go back to the previous page in browser history.",
377
+ inputSchema: backInputSchema,
378
+ execute: async (_input, { agent }) => {
379
+ const threadId = agent?.threadId;
380
+ browser.setCurrentThread(threadId);
381
+ await browser.ensureReady();
382
+ return browser.back(threadId);
383
+ }
384
+ });
298
385
  }
386
+ //#endregion
387
+ //#region src/tools/click.ts
388
+ /**
389
+ * browser_click - Click an element
390
+ */
299
391
  function createClickTool(browser) {
300
- return tools.createTool({
301
- id: BROWSER_TOOLS.CLICK,
302
- description: "Click an element using its ref from a snapshot. Use clickCount: 2 for double-click. Pass waitUntil when the click triggers navigation so the page settles before the next snapshot.",
303
- inputSchema: clickInputSchema,
304
- execute: async (input, { agent }) => {
305
- const threadId = agent?.threadId;
306
- browser.setCurrentThread(threadId);
307
- await browser.ensureReady();
308
- return browser.click(input, threadId);
309
- }
310
- });
392
+ return (0, _mastra_core_tools.createTool)({
393
+ id: BROWSER_TOOLS.CLICK,
394
+ description: "Click an element using its ref from a snapshot. Use clickCount: 2 for double-click. Pass waitUntil when the click triggers navigation so the page settles before the next snapshot.",
395
+ inputSchema: clickInputSchema,
396
+ execute: async (input, { agent }) => {
397
+ const threadId = agent?.threadId;
398
+ browser.setCurrentThread(threadId);
399
+ await browser.ensureReady();
400
+ return browser.click(input, threadId);
401
+ }
402
+ });
311
403
  }
404
+ //#endregion
405
+ //#region src/tools/close.ts
406
+ /**
407
+ * browser_close - Close the browser
408
+ */
312
409
  function createCloseTool(browser) {
313
- return tools.createTool({
314
- id: BROWSER_TOOLS.CLOSE,
315
- description: "Close the browser. Only use when done with all browsing.",
316
- inputSchema: closeInputSchema,
317
- execute: async (_input, { agent }) => {
318
- const threadId = agent?.threadId;
319
- browser.setCurrentThread(threadId);
320
- if (browser.getScope() !== "shared") {
321
- if (!threadId) {
322
- throw new Error("browser_close requires agent.threadId when browser scope is not shared");
323
- }
324
- browser.markBrowserCloseReason("agent", threadId);
325
- await browser.closeThreadSession(threadId);
326
- return { success: true, hint: "Thread's browser session closed. A new session will be created on next use." };
327
- }
328
- browser.markBrowserCloseReason("agent");
329
- await browser.close();
330
- return { success: true, hint: "Browser closed. It will be re-launched automatically on next use." };
331
- }
332
- });
410
+ return (0, _mastra_core_tools.createTool)({
411
+ id: BROWSER_TOOLS.CLOSE,
412
+ description: "Close the browser. Only use when done with all browsing.",
413
+ inputSchema: closeInputSchema,
414
+ execute: async (_input, { agent }) => {
415
+ const threadId = agent?.threadId;
416
+ browser.setCurrentThread(threadId);
417
+ if (browser.getScope() !== "shared") {
418
+ if (!threadId) throw new Error("browser_close requires agent.threadId when browser scope is not shared");
419
+ browser.markBrowserCloseReason("agent", threadId);
420
+ await browser.closeThreadSession(threadId);
421
+ return {
422
+ success: true,
423
+ hint: "Thread's browser session closed. A new session will be created on next use."
424
+ };
425
+ }
426
+ browser.markBrowserCloseReason("agent");
427
+ await browser.close();
428
+ return {
429
+ success: true,
430
+ hint: "Browser closed. It will be re-launched automatically on next use."
431
+ };
432
+ }
433
+ });
333
434
  }
435
+ //#endregion
436
+ //#region src/tools/dialog.ts
437
+ /**
438
+ * browser_dialog - Click element and handle resulting dialog
439
+ */
334
440
  function createDialogTool(browser) {
335
- return tools.createTool({
336
- id: BROWSER_TOOLS.DIALOG,
337
- description: "Click an element that triggers a browser dialog (alert, confirm, prompt) and handle it. Use this instead of browser_click when you expect a dialog to appear.",
338
- inputSchema: dialogInputSchema,
339
- execute: async (input, { agent }) => {
340
- const threadId = agent?.threadId;
341
- browser.setCurrentThread(threadId);
342
- await browser.ensureReady();
343
- return browser.dialog(input, threadId);
344
- }
345
- });
441
+ return (0, _mastra_core_tools.createTool)({
442
+ id: BROWSER_TOOLS.DIALOG,
443
+ description: "Click an element that triggers a browser dialog (alert, confirm, prompt) and handle it. Use this instead of browser_click when you expect a dialog to appear.",
444
+ inputSchema: dialogInputSchema,
445
+ execute: async (input, { agent }) => {
446
+ const threadId = agent?.threadId;
447
+ browser.setCurrentThread(threadId);
448
+ await browser.ensureReady();
449
+ return browser.dialog(input, threadId);
450
+ }
451
+ });
346
452
  }
453
+ //#endregion
454
+ //#region src/tools/drag.ts
455
+ /**
456
+ * browser_drag - Drag an element to another element
457
+ */
347
458
  function createDragTool(browser) {
348
- return tools.createTool({
349
- id: BROWSER_TOOLS.DRAG,
350
- description: "Drag an element to another element. Use refs from snapshot when available, or CSS selectors for elements not exposed in the accessibility tree.",
351
- inputSchema: dragInputSchema,
352
- execute: async (input, { agent }) => {
353
- const threadId = agent?.threadId;
354
- browser.setCurrentThread(threadId);
355
- await browser.ensureReady();
356
- return browser.drag(input, threadId);
357
- }
358
- });
459
+ return (0, _mastra_core_tools.createTool)({
460
+ id: BROWSER_TOOLS.DRAG,
461
+ description: "Drag an element to another element. Use refs from snapshot when available, or CSS selectors for elements not exposed in the accessibility tree.",
462
+ inputSchema: dragInputSchema,
463
+ execute: async (input, { agent }) => {
464
+ const threadId = agent?.threadId;
465
+ browser.setCurrentThread(threadId);
466
+ await browser.ensureReady();
467
+ return browser.drag(input, threadId);
468
+ }
469
+ });
359
470
  }
471
+ //#endregion
472
+ //#region src/tools/evaluate.ts
473
+ /**
474
+ * browser_evaluate - Execute JavaScript in the browser
475
+ */
360
476
  function createEvaluateTool(browser) {
361
- return tools.createTool({
362
- id: BROWSER_TOOLS.EVALUATE,
363
- description: "Execute JavaScript in the browser. Use for complex interactions not covered by other tools. Returns the script result.",
364
- inputSchema: evaluateInputSchema,
365
- execute: async (input, { agent }) => {
366
- const threadId = agent?.threadId;
367
- browser.setCurrentThread(threadId);
368
- await browser.ensureReady();
369
- return browser.evaluate(input, threadId);
370
- }
371
- });
477
+ return (0, _mastra_core_tools.createTool)({
478
+ id: BROWSER_TOOLS.EVALUATE,
479
+ description: "Execute JavaScript in the browser. Use for complex interactions not covered by other tools. Returns the script result.",
480
+ inputSchema: evaluateInputSchema,
481
+ execute: async (input, { agent }) => {
482
+ const threadId = agent?.threadId;
483
+ browser.setCurrentThread(threadId);
484
+ await browser.ensureReady();
485
+ return browser.evaluate(input, threadId);
486
+ }
487
+ });
372
488
  }
489
+ //#endregion
490
+ //#region src/tools/goto.ts
491
+ /**
492
+ * browser_goto - Navigate to a URL
493
+ */
373
494
  function createGotoTool(browser) {
374
- return tools.createTool({
375
- id: BROWSER_TOOLS.GOTO,
376
- description: "Navigate the browser to a URL.",
377
- inputSchema: gotoInputSchema,
378
- execute: async (input, { agent }) => {
379
- const threadId = agent?.threadId;
380
- browser.setCurrentThread(threadId);
381
- await browser.ensureReady();
382
- return browser.goto(input, threadId);
383
- }
384
- });
495
+ return (0, _mastra_core_tools.createTool)({
496
+ id: BROWSER_TOOLS.GOTO,
497
+ description: "Navigate the browser to a URL.",
498
+ inputSchema: gotoInputSchema,
499
+ execute: async (input, { agent }) => {
500
+ const threadId = agent?.threadId;
501
+ browser.setCurrentThread(threadId);
502
+ await browser.ensureReady();
503
+ return browser.goto(input, threadId);
504
+ }
505
+ });
385
506
  }
507
+ //#endregion
508
+ //#region src/tools/hover.ts
509
+ /**
510
+ * browser_hover - Hover over an element
511
+ */
386
512
  function createHoverTool(browser) {
387
- return tools.createTool({
388
- id: BROWSER_TOOLS.HOVER,
389
- description: "Hover over an element to trigger hover states (dropdowns, tooltips).",
390
- inputSchema: hoverInputSchema,
391
- execute: async (input, { agent }) => {
392
- const threadId = agent?.threadId;
393
- browser.setCurrentThread(threadId);
394
- await browser.ensureReady();
395
- return browser.hover(input, threadId);
396
- }
397
- });
513
+ return (0, _mastra_core_tools.createTool)({
514
+ id: BROWSER_TOOLS.HOVER,
515
+ description: "Hover over an element to trigger hover states (dropdowns, tooltips).",
516
+ inputSchema: hoverInputSchema,
517
+ execute: async (input, { agent }) => {
518
+ const threadId = agent?.threadId;
519
+ browser.setCurrentThread(threadId);
520
+ await browser.ensureReady();
521
+ return browser.hover(input, threadId);
522
+ }
523
+ });
398
524
  }
525
+ //#endregion
526
+ //#region src/tools/press.ts
527
+ /**
528
+ * browser_press - Press a keyboard key
529
+ */
399
530
  function createPressTool(browser) {
400
- return tools.createTool({
401
- id: BROWSER_TOOLS.PRESS,
402
- description: "Press a keyboard key (e.g., Enter, Tab, Escape, Control+a). Pass waitUntil when the keypress triggers navigation (e.g., Enter to submit a form) so the page settles before the next snapshot.",
403
- inputSchema: pressInputSchema,
404
- execute: async (input, { agent }) => {
405
- const threadId = agent?.threadId;
406
- browser.setCurrentThread(threadId);
407
- await browser.ensureReady();
408
- return browser.press(input, threadId);
409
- }
410
- });
531
+ return (0, _mastra_core_tools.createTool)({
532
+ id: BROWSER_TOOLS.PRESS,
533
+ description: "Press a keyboard key (e.g., Enter, Tab, Escape, Control+a). Pass waitUntil when the keypress triggers navigation (e.g., Enter to submit a form) so the page settles before the next snapshot.",
534
+ inputSchema: pressInputSchema,
535
+ execute: async (input, { agent }) => {
536
+ const threadId = agent?.threadId;
537
+ browser.setCurrentThread(threadId);
538
+ await browser.ensureReady();
539
+ return browser.press(input, threadId);
540
+ }
541
+ });
411
542
  }
543
+ //#endregion
544
+ //#region src/tools/screenshot.ts
545
+ /**
546
+ * browser_screenshot - Capture a screenshot of the current page
547
+ */
412
548
  function createScreenshotTool(browser) {
413
- return tools.createTool({
414
- id: BROWSER_TOOLS.SCREENSHOT,
415
- description: "Capture a screenshot of the current viewport as a visible PNG (set fullPage: true for full-page capture). Use snapshot 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.",
416
- inputSchema: screenshotInputSchema,
417
- execute: async (input, { agent }) => {
418
- const threadId = agent?.threadId;
419
- browser.setCurrentThread(threadId);
420
- await browser.ensureReady();
421
- return await browser.screenshot(input, threadId);
422
- },
423
- toModelOutput(output) {
424
- const result = output;
425
- if (typeof result.base64 !== "string") {
426
- return {
427
- type: "content",
428
- value: [{ type: "text", text: result.message ?? "Failed to capture screenshot." }]
429
- };
430
- }
431
- return {
432
- type: "content",
433
- value: [
434
- {
435
- type: "media",
436
- mediaType: "image/png",
437
- data: result.base64
438
- }
439
- ]
440
- };
441
- }
442
- });
549
+ return (0, _mastra_core_tools.createTool)({
550
+ id: BROWSER_TOOLS.SCREENSHOT,
551
+ description: "Capture a screenshot of the current viewport as a visible PNG (set fullPage: true for full-page capture). Use snapshot 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.",
552
+ inputSchema: screenshotInputSchema,
553
+ execute: async (input, { agent }) => {
554
+ const threadId = agent?.threadId;
555
+ browser.setCurrentThread(threadId);
556
+ await browser.ensureReady();
557
+ return await browser.screenshot(input, threadId);
558
+ },
559
+ toModelOutput(output) {
560
+ const result = output;
561
+ if (typeof result.base64 !== "string") return {
562
+ type: "content",
563
+ value: [{
564
+ type: "text",
565
+ text: result.message ?? "Failed to capture screenshot."
566
+ }]
567
+ };
568
+ return {
569
+ type: "content",
570
+ value: [{
571
+ type: "media",
572
+ mediaType: "image/png",
573
+ data: result.base64
574
+ }]
575
+ };
576
+ }
577
+ });
443
578
  }
579
+ //#endregion
580
+ //#region src/tools/scroll.ts
581
+ /**
582
+ * browser_scroll - Scroll the page or element
583
+ */
444
584
  function createScrollTool(browser) {
445
- return tools.createTool({
446
- id: BROWSER_TOOLS.SCROLL,
447
- description: "Scroll the page or a specific element.",
448
- inputSchema: scrollInputSchema,
449
- execute: async (input, { agent }) => {
450
- const threadId = agent?.threadId;
451
- browser.setCurrentThread(threadId);
452
- await browser.ensureReady();
453
- return browser.scroll(input, threadId);
454
- }
455
- });
585
+ return (0, _mastra_core_tools.createTool)({
586
+ id: BROWSER_TOOLS.SCROLL,
587
+ description: "Scroll the page or a specific element.",
588
+ inputSchema: scrollInputSchema,
589
+ execute: async (input, { agent }) => {
590
+ const threadId = agent?.threadId;
591
+ browser.setCurrentThread(threadId);
592
+ await browser.ensureReady();
593
+ return browser.scroll(input, threadId);
594
+ }
595
+ });
456
596
  }
597
+ //#endregion
598
+ //#region src/tools/select.ts
599
+ /**
600
+ * browser_select - Select option from dropdown
601
+ */
457
602
  function createSelectTool(browser) {
458
- return tools.createTool({
459
- id: BROWSER_TOOLS.SELECT,
460
- description: "Select an option from a dropdown by value, label, or index. Pass waitUntil when the selection triggers navigation so the page settles before the next snapshot.",
461
- inputSchema: selectInputSchema,
462
- execute: async (input, { agent }) => {
463
- const threadId = agent?.threadId;
464
- browser.setCurrentThread(threadId);
465
- await browser.ensureReady();
466
- return browser.select(input, threadId);
467
- }
468
- });
603
+ return (0, _mastra_core_tools.createTool)({
604
+ id: BROWSER_TOOLS.SELECT,
605
+ description: "Select an option from a dropdown by value, label, or index. Pass waitUntil when the selection triggers navigation so the page settles before the next snapshot.",
606
+ inputSchema: selectInputSchema,
607
+ execute: async (input, { agent }) => {
608
+ const threadId = agent?.threadId;
609
+ browser.setCurrentThread(threadId);
610
+ await browser.ensureReady();
611
+ return browser.select(input, threadId);
612
+ }
613
+ });
469
614
  }
615
+ //#endregion
616
+ //#region src/tools/snapshot.ts
617
+ /**
618
+ * browser_snapshot - Get accessibility tree snapshot
619
+ */
470
620
  function createSnapshotTool(browser) {
471
- return tools.createTool({
472
- id: BROWSER_TOOLS.SNAPSHOT,
473
- description: "Get accessibility tree snapshot of the page. Returns text-based representation with element refs like [ref=e1], [ref=e2] for targeting.",
474
- inputSchema: snapshotInputSchema,
475
- execute: async (input, { agent }) => {
476
- const threadId = agent?.threadId;
477
- browser.setCurrentThread(threadId);
478
- await browser.ensureReady();
479
- return browser.snapshot(input, threadId);
480
- }
481
- });
621
+ return (0, _mastra_core_tools.createTool)({
622
+ id: BROWSER_TOOLS.SNAPSHOT,
623
+ description: "Get accessibility tree snapshot of the page. Returns text-based representation with element refs like [ref=e1], [ref=e2] for targeting.",
624
+ inputSchema: snapshotInputSchema,
625
+ execute: async (input, { agent }) => {
626
+ const threadId = agent?.threadId;
627
+ browser.setCurrentThread(threadId);
628
+ await browser.ensureReady();
629
+ return browser.snapshot(input, threadId);
630
+ }
631
+ });
482
632
  }
633
+ //#endregion
634
+ //#region src/tools/tabs.ts
635
+ /**
636
+ * browser_tabs - Manage browser tabs
637
+ */
483
638
  function createTabsTool(browser) {
484
- return tools.createTool({
485
- id: BROWSER_TOOLS.TABS,
486
- description: "Manage browser tabs: list, open new, switch, or close tabs.",
487
- inputSchema: tabsInputSchema,
488
- execute: async (input, { agent }) => {
489
- const threadId = agent?.threadId;
490
- browser.setCurrentThread(threadId);
491
- await browser.ensureReady();
492
- return browser.tabs(input, threadId);
493
- }
494
- });
639
+ return (0, _mastra_core_tools.createTool)({
640
+ id: BROWSER_TOOLS.TABS,
641
+ description: "Manage browser tabs: list, open new, switch, or close tabs.",
642
+ inputSchema: tabsInputSchema,
643
+ execute: async (input, { agent }) => {
644
+ const threadId = agent?.threadId;
645
+ browser.setCurrentThread(threadId);
646
+ await browser.ensureReady();
647
+ return browser.tabs(input, threadId);
648
+ }
649
+ });
495
650
  }
651
+ //#endregion
652
+ //#region src/tools/type.ts
653
+ /**
654
+ * browser_type - Type text into an element
655
+ */
496
656
  function createTypeTool(browser) {
497
- return tools.createTool({
498
- id: BROWSER_TOOLS.TYPE,
499
- description: "Type text into an input element. Use clear: true to replace existing content.",
500
- inputSchema: typeInputSchema,
501
- execute: async (input, { agent }) => {
502
- const threadId = agent?.threadId;
503
- browser.setCurrentThread(threadId);
504
- await browser.ensureReady();
505
- return browser.type(input, threadId);
506
- }
507
- });
657
+ return (0, _mastra_core_tools.createTool)({
658
+ id: BROWSER_TOOLS.TYPE,
659
+ description: "Type text into an input element. Use clear: true to replace existing content.",
660
+ inputSchema: typeInputSchema,
661
+ execute: async (input, { agent }) => {
662
+ const threadId = agent?.threadId;
663
+ browser.setCurrentThread(threadId);
664
+ await browser.ensureReady();
665
+ return browser.type(input, threadId);
666
+ }
667
+ });
508
668
  }
669
+ //#endregion
670
+ //#region src/tools/wait.ts
671
+ /**
672
+ * browser_wait - Wait for an element or condition
673
+ */
509
674
  function createWaitTool(browser) {
510
- return tools.createTool({
511
- id: BROWSER_TOOLS.WAIT,
512
- description: "Wait for an element to appear, disappear, or reach a state.",
513
- inputSchema: waitInputSchema,
514
- execute: async (input, { agent }) => {
515
- const threadId = agent?.threadId;
516
- browser.setCurrentThread(threadId);
517
- await browser.ensureReady();
518
- return browser.wait(input, threadId);
519
- }
520
- });
675
+ return (0, _mastra_core_tools.createTool)({
676
+ id: BROWSER_TOOLS.WAIT,
677
+ description: "Wait for an element to appear, disappear, or reach a state.",
678
+ inputSchema: waitInputSchema,
679
+ execute: async (input, { agent }) => {
680
+ const threadId = agent?.threadId;
681
+ browser.setCurrentThread(threadId);
682
+ await browser.ensureReady();
683
+ return browser.wait(input, threadId);
684
+ }
685
+ });
521
686
  }
522
-
523
- // src/tools/index.ts
687
+ //#endregion
688
+ //#region src/tools/index.ts
689
+ /**
690
+ * Creates all browser tools bound to an AgentBrowser instance.
691
+ * The browser is lazily initialized on first tool use.
692
+ */
524
693
  function createAgentBrowserTools(browser) {
525
- return {
526
- // Core (9)
527
- [BROWSER_TOOLS.GOTO]: createGotoTool(browser),
528
- [BROWSER_TOOLS.SNAPSHOT]: createSnapshotTool(browser),
529
- [BROWSER_TOOLS.CLICK]: createClickTool(browser),
530
- [BROWSER_TOOLS.TYPE]: createTypeTool(browser),
531
- [BROWSER_TOOLS.PRESS]: createPressTool(browser),
532
- [BROWSER_TOOLS.SELECT]: createSelectTool(browser),
533
- [BROWSER_TOOLS.SCROLL]: createScrollTool(browser),
534
- [BROWSER_TOOLS.CLOSE]: createCloseTool(browser),
535
- // Utility
536
- [BROWSER_TOOLS.SCREENSHOT]: createScreenshotTool(browser),
537
- // Extended
538
- [BROWSER_TOOLS.HOVER]: createHoverTool(browser),
539
- [BROWSER_TOOLS.BACK]: createBackTool(browser),
540
- [BROWSER_TOOLS.DIALOG]: createDialogTool(browser),
541
- [BROWSER_TOOLS.WAIT]: createWaitTool(browser),
542
- [BROWSER_TOOLS.TABS]: createTabsTool(browser),
543
- [BROWSER_TOOLS.DRAG]: createDragTool(browser),
544
- // Escape hatch (1)
545
- [BROWSER_TOOLS.EVALUATE]: createEvaluateTool(browser)
546
- };
694
+ return {
695
+ [BROWSER_TOOLS.GOTO]: createGotoTool(browser),
696
+ [BROWSER_TOOLS.SNAPSHOT]: createSnapshotTool(browser),
697
+ [BROWSER_TOOLS.CLICK]: createClickTool(browser),
698
+ [BROWSER_TOOLS.TYPE]: createTypeTool(browser),
699
+ [BROWSER_TOOLS.PRESS]: createPressTool(browser),
700
+ [BROWSER_TOOLS.SELECT]: createSelectTool(browser),
701
+ [BROWSER_TOOLS.SCROLL]: createScrollTool(browser),
702
+ [BROWSER_TOOLS.CLOSE]: createCloseTool(browser),
703
+ [BROWSER_TOOLS.SCREENSHOT]: createScreenshotTool(browser),
704
+ [BROWSER_TOOLS.HOVER]: createHoverTool(browser),
705
+ [BROWSER_TOOLS.BACK]: createBackTool(browser),
706
+ [BROWSER_TOOLS.DIALOG]: createDialogTool(browser),
707
+ [BROWSER_TOOLS.WAIT]: createWaitTool(browser),
708
+ [BROWSER_TOOLS.TABS]: createTabsTool(browser),
709
+ [BROWSER_TOOLS.DRAG]: createDragTool(browser),
710
+ [BROWSER_TOOLS.EVALUATE]: createEvaluateTool(browser)
711
+ };
547
712
  }
548
-
549
- // src/utils.ts
713
+ //#endregion
714
+ //#region src/utils.ts
715
+ /**
716
+ * Get the browser process PID from a BrowserManager instance via CDP.
717
+ *
718
+ * Playwright doesn't expose the browser process PID directly, so we use CDP's
719
+ * SystemInfo.getProcessInfo to get it. This works for both regular browser
720
+ * launches and persistent contexts (profiles).
721
+ *
722
+ * Returns undefined if the PID can't be retrieved (e.g., browser not running).
723
+ */
550
724
  async function getBrowserPid(manager) {
551
- try {
552
- let browser = manager.getBrowser();
553
- if (!browser) {
554
- const ctx = manager.getContext();
555
- browser = ctx?.browser?.() ?? null;
556
- }
557
- if (!browser) return void 0;
558
- const cdp = await browser.newBrowserCDPSession();
559
- try {
560
- const info = await cdp.send("SystemInfo.getProcessInfo");
561
- const browserProcess = info.processInfo?.find((p) => p.type === "browser");
562
- return browserProcess?.id;
563
- } finally {
564
- await cdp.detach().catch(() => void 0);
565
- }
566
- } catch {
567
- return void 0;
568
- }
725
+ try {
726
+ let browser = manager.getBrowser();
727
+ if (!browser) browser = manager.getContext()?.browser?.() ?? null;
728
+ if (!browser) return void 0;
729
+ const cdp = await browser.newBrowserCDPSession();
730
+ try {
731
+ return ((await cdp.send("SystemInfo.getProcessInfo")).processInfo?.find((p) => p.type === "browser"))?.id;
732
+ } finally {
733
+ await cdp.detach().catch(() => void 0);
734
+ }
735
+ } catch {
736
+ return;
737
+ }
569
738
  }
570
-
571
- // src/agent-browser.ts
572
- var AgentBrowser = class extends browser.MastraBrowser {
573
- id;
574
- name = "AgentBrowser";
575
- provider = "vercel-labs/agent-browser";
576
- defaultTimeout = 3e4;
577
- /** Pending PID lookups awaited in disconnect handlers to avoid racing. */
578
- pidLookups = /* @__PURE__ */ new Set();
579
- pendingCloseReasons = /* @__PURE__ */ new Map();
580
- activeUrlChangeSources = /* @__PURE__ */ new Map();
581
- browserConfig;
582
- constructor(config = {}) {
583
- super(config);
584
- this.browserConfig = config;
585
- this.id = `agent-browser-${Date.now()}`;
586
- if (config.timeout) {
587
- this.defaultTimeout = config.timeout;
588
- }
589
- const effectiveScope = config.cdpUrl ? config.scope ?? "shared" : config.scope ?? "thread";
590
- const threadManagerConfig = {
591
- scope: effectiveScope,
592
- browserConfig: { ...config, headless: this.headless },
593
- resolveCdpUrl: this.resolveCdpUrl.bind(this),
594
- logger: this.logger,
595
- // When a new thread session is created, notify listeners so screencast can start
596
- onSessionCreated: (session) => {
597
- this.notifyBrowserReady(session.threadId);
598
- },
599
- // When a new browser is created for a thread, set up close listener
600
- onBrowserCreated: (manager, threadId) => {
601
- this.setupCloseListenerForThread(manager, threadId);
602
- }
603
- };
604
- const createTm = config.createThreadManager ?? ((opts) => new AgentBrowserThreadManager(opts));
605
- this.threadManager = createTm(threadManagerConfig);
606
- }
607
- // ---------------------------------------------------------------------------
608
- // Thread Scope (delegated to ThreadManager)
609
- // ---------------------------------------------------------------------------
610
- /**
611
- * Ensure browser is ready and thread session exists.
612
- * Creates a new page/context for the current thread if needed.
613
- *
614
- * For 'thread' scope, we need to create the thread session BEFORE
615
- * calling super.ensureReady() because the base class's ensureReady() will
616
- * call checkBrowserAlive(), which needs at least one thread browser to exist.
617
- */
618
- async ensureReady() {
619
- const scope = this.threadManager.getScope();
620
- const threadId = this.getCurrentThread();
621
- const existingSession = this.threadManager.hasSession(threadId);
622
- if (scope === "thread" && !existingSession) {
623
- await this.getManagerForThread(threadId);
624
- }
625
- await super.ensureReady();
626
- if (scope === "thread" && existingSession) {
627
- await this.getManagerForThread(threadId);
628
- }
629
- }
630
- /**
631
- * Get the browser manager for the current thread.
632
- * Delegates to ThreadManager for scope handling.
633
- */
634
- async getManagerForThread(threadId) {
635
- const effectiveThreadId = threadId ?? this.getCurrentThread();
636
- const scope = this.threadManager.getScope();
637
- if (scope === "thread" && (!effectiveThreadId || effectiveThreadId === browser.DEFAULT_THREAD_ID)) {
638
- const existingManager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
639
- if (existingManager) {
640
- return existingManager;
641
- }
642
- }
643
- return this.threadManager.getManagerForThread(effectiveThreadId);
644
- }
645
- // ---------------------------------------------------------------------------
646
- // Lifecycle
647
- // ---------------------------------------------------------------------------
648
- async doLaunch() {
649
- this.pendingCloseReasons.clear();
650
- this.activeUrlChangeSources.clear();
651
- const scope = this.threadManager.getScope();
652
- if (scope === "thread") {
653
- this.sharedManager = new agentBrowser.BrowserManager();
654
- this.threadManager.setSharedManager(this.sharedManager);
655
- return;
656
- }
657
- this.sharedManager = new agentBrowser.BrowserManager();
658
- const localConfig = this.config;
659
- const launchOptions = {
660
- headless: this.headless,
661
- viewport: localConfig.viewport,
662
- profile: localConfig.profile,
663
- executablePath: localConfig.executablePath,
664
- storageState: localConfig.storageState
665
- };
666
- if (localConfig.cdpUrl) {
667
- launchOptions.cdpUrl = await this.resolveCdpUrl(localConfig.cdpUrl);
668
- }
669
- await this.sharedManager.launch(launchOptions);
670
- this.threadManager.setSharedManager(this.sharedManager);
671
- this.setupCloseListenerForSharedScope(this.sharedManager);
672
- }
673
- /**
674
- * Set up close event listeners for 'shared' scope browser.
675
- * This handles the case where the shared browser is closed externally.
676
- */
677
- setupCloseListenerForSharedScope(manager) {
678
- try {
679
- const pidLookup = getBrowserPid(manager).then((pid) => {
680
- if (pid && this.sharedManager === manager) this.sharedBrowserPid = pid;
681
- }).finally(() => this.pidLookups.delete(pidLookup));
682
- this.pidLookups.add(pidLookup);
683
- let disconnectHandled = false;
684
- const handleDisconnect = () => {
685
- if (disconnectHandled) return;
686
- disconnectHandled = true;
687
- this.rememberClosedBrowserState(manager, "user");
688
- void pidLookup.catch(() => void 0).then(() => this.handleBrowserDisconnected());
689
- };
690
- const context = manager.getContext();
691
- if (context) {
692
- context.on("close", handleDisconnect);
693
- }
694
- const pages = manager.getPages();
695
- for (const page of pages) {
696
- page.on("close", () => {
697
- const remainingPages = manager.getPages();
698
- if (remainingPages.length === 0) {
699
- handleDisconnect();
700
- }
701
- });
702
- }
703
- } catch {
704
- }
705
- }
706
- async doClose() {
707
- await Promise.allSettled([...this.pidLookups]);
708
- this.pidLookups.clear();
709
- await this.threadManager.destroyAllSessions();
710
- this.setCurrentThread(void 0);
711
- const scope = this.threadManager.getScope();
712
- if (scope === "shared" && this.sharedManager) {
713
- await this.sharedManager.close();
714
- }
715
- this.sharedManager = null;
716
- }
717
- async closeThreadSession(threadId) {
718
- const manager = this.threadManager.getExistingManagerForThread(threadId);
719
- if (manager) {
720
- const state = this.getBrowserStateForManager(manager, threadId);
721
- if (state) this.threadManager.updateBrowserState(threadId, state);
722
- }
723
- await super.closeThreadSession(threadId);
724
- }
725
- /**
726
- * Check if the browser is still alive by verifying the page is connected.
727
- * Called by base class ensureReady() to detect externally closed browsers.
728
- */
729
- async checkBrowserAlive() {
730
- const scope = this.threadManager.getScope();
731
- if (scope === "thread") {
732
- return this.threadManager.hasActiveThreadManagers();
733
- }
734
- if (!this.sharedManager) {
735
- return false;
736
- }
737
- try {
738
- const page = this.sharedManager.getPage();
739
- const url = page.url();
740
- if (url && url !== "about:blank") {
741
- const state = await this.getBrowserState();
742
- if (state) {
743
- this.lastBrowserState = state;
744
- }
745
- }
746
- return true;
747
- } catch (error) {
748
- const msg = error instanceof Error ? error.message : String(error);
749
- if (this.isDisconnectionError(msg)) {
750
- this.logger.debug?.("Browser was externally closed");
751
- }
752
- return false;
753
- }
754
- }
755
- // ---------------------------------------------------------------------------
756
- // Tools
757
- // ---------------------------------------------------------------------------
758
- /**
759
- * Get the browser tools for this provider.
760
- * Returns 16 flat tools for browser automation.
761
- */
762
- getTools() {
763
- const tools = createAgentBrowserTools(this);
764
- if (this.browserConfig.recording) {
765
- Object.assign(tools, browser.createBrowserRecordingTools(this, this.browserConfig.recording));
766
- }
767
- const exclude = this.browserConfig.excludeTools;
768
- if (exclude?.length) {
769
- for (const name of exclude) {
770
- delete tools[name];
771
- }
772
- }
773
- return tools;
774
- }
775
- // ---------------------------------------------------------------------------
776
- // Helpers
777
- // ---------------------------------------------------------------------------
778
- browserStateKey(threadId) {
779
- return threadId ?? this.getCurrentThread() ?? browser.DEFAULT_THREAD_ID;
780
- }
781
- markBrowserCloseReason(reason, threadId) {
782
- this.pendingCloseReasons.set(this.browserStateKey(threadId), reason);
783
- }
784
- markActiveUrlChangeSource(source, url, threadId) {
785
- this.activeUrlChangeSources.set(this.browserStateKey(threadId), { url, source });
786
- }
787
- getCloseReason(threadId) {
788
- return this.pendingCloseReasons.get(this.browserStateKey(threadId)) ?? this.pendingCloseReasons.get(browser.DEFAULT_THREAD_ID);
789
- }
790
- getActiveUrlChangeSource(activeUrl, threadId) {
791
- const entry = this.activeUrlChangeSources.get(this.browserStateKey(threadId));
792
- return entry && entry.url === activeUrl ? entry.source : void 0;
793
- }
794
- rememberClosedBrowserState(manager, reason, threadId) {
795
- const state = this.getBrowserStateForManager(manager, threadId);
796
- if (!state || state.tabs.length === 0) return;
797
- const closedState = { ...state, closeReason: this.getCloseReason(threadId) ?? reason };
798
- if (threadId) {
799
- this.threadManager.updateBrowserState(threadId, closedState);
800
- } else {
801
- this.lastBrowserState = closedState;
802
- }
803
- }
804
- /**
805
- * Get the page for the current thread.
806
- * Uses thread scope if enabled, otherwise returns the shared page.
807
- * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
808
- * Use this to avoid race conditions in concurrent tool calls.
809
- */
810
- async getPage(explicitThreadId) {
811
- const scope = this.getScope();
812
- const threadId = explicitThreadId ?? this.getCurrentThread();
813
- if (scope === "thread") {
814
- return this.threadManager.getPageForThread(threadId);
815
- }
816
- if (!this.sharedManager) throw new Error("Browser not launched");
817
- return this.sharedManager.getPage();
818
- }
819
- /**
820
- * Get the active page for a thread (implements abstract method from base class).
821
- * Returns null if no page is available, unlike getPage which throws.
822
- */
823
- async getActivePage(threadId) {
824
- try {
825
- return await this.getPage(threadId);
826
- } catch {
827
- return null;
828
- }
829
- }
830
- /**
831
- * Set up close event listener for a thread's browser manager.
832
- * This handles the case where a thread's browser is closed externally.
833
- */
834
- setupCloseListenerForThread(manager, threadId) {
835
- try {
836
- const pidLookup = getBrowserPid(manager).then((pid) => {
837
- if (pid && this.threadManager?.getExistingManagerForThread(threadId) === manager) {
838
- this.threadBrowserPids.set(threadId, pid);
839
- }
840
- }).finally(() => this.pidLookups.delete(pidLookup));
841
- this.pidLookups.add(pidLookup);
842
- let disconnectHandled = false;
843
- const handleDisconnect = () => {
844
- if (disconnectHandled) return;
845
- disconnectHandled = true;
846
- this.rememberClosedBrowserState(manager, "user", threadId);
847
- void pidLookup.catch(() => void 0).then(() => this.handleThreadBrowserDisconnected(threadId));
848
- };
849
- const context = manager.getContext();
850
- if (context) {
851
- context.on("close", handleDisconnect);
852
- }
853
- const pages = manager.getPages();
854
- for (const page of pages) {
855
- page.on("close", () => {
856
- const remainingPages = manager.getPages();
857
- if (remainingPages.length === 0) {
858
- handleDisconnect();
859
- }
860
- });
861
- }
862
- } catch {
863
- }
864
- }
865
- /**
866
- * Create an error response from an exception.
867
- * Extends base class to add agent-browser specific error handling.
868
- */
869
- createErrorFromException(error, context) {
870
- const msg = error instanceof Error ? error.message : String(error);
871
- if (msg.includes("stale") || msg.includes("Stale")) {
872
- return this.createError(
873
- "stale_ref",
874
- "Element ref is no longer valid.",
875
- "Get a fresh snapshot and use updated refs."
876
- );
877
- }
878
- if (msg.includes("not found") || msg.includes("No element")) {
879
- return this.createError(
880
- "element_not_found",
881
- "Element not found.",
882
- "Check the ref is correct or get a fresh snapshot."
883
- );
884
- }
885
- return super.createErrorFromException(error, context);
886
- }
887
- async requireLocator(ref, threadId) {
888
- const manager = await this.getManagerForThread(threadId);
889
- return manager.getLocatorFromRef(ref);
890
- }
891
- async getScrollInfo(threadId) {
892
- const page = await this.getPage(threadId);
893
- const info = await page.evaluate(`({
739
+ //#endregion
740
+ //#region src/agent-browser.ts
741
+ /**
742
+ * AgentBrowser - Browser automation using agent-browser (vercel-labs/agent-browser)
743
+ *
744
+ * Uses snapshot + refs pattern for LLM-friendly element targeting.
745
+ */
746
+ var AgentBrowser = class extends _mastra_core_browser.MastraBrowser {
747
+ id;
748
+ name = "AgentBrowser";
749
+ provider = "vercel-labs/agent-browser";
750
+ defaultTimeout = 3e4;
751
+ /** Pending PID lookups — awaited in disconnect handlers to avoid racing. */
752
+ pidLookups = /* @__PURE__ */ new Set();
753
+ pendingCloseReasons = /* @__PURE__ */ new Map();
754
+ activeUrlChangeSources = /* @__PURE__ */ new Map();
755
+ browserConfig;
756
+ constructor(config = {}) {
757
+ super(config);
758
+ this.browserConfig = config;
759
+ this.id = `agent-browser-${Date.now()}`;
760
+ if (config.timeout) this.defaultTimeout = config.timeout;
761
+ const threadManagerConfig = {
762
+ scope: config.cdpUrl ? config.scope ?? "shared" : config.scope ?? "thread",
763
+ browserConfig: {
764
+ ...config,
765
+ headless: this.headless
766
+ },
767
+ resolveCdpUrl: this.resolveCdpUrl.bind(this),
768
+ logger: this.logger,
769
+ onSessionCreated: (session) => {
770
+ this.notifyBrowserReady(session.threadId);
771
+ },
772
+ onBrowserCreated: (manager, threadId) => {
773
+ this.setupCloseListenerForThread(manager, threadId);
774
+ }
775
+ };
776
+ const createTm = config.createThreadManager ?? ((opts) => new AgentBrowserThreadManager(opts));
777
+ this.threadManager = createTm(threadManagerConfig);
778
+ }
779
+ /**
780
+ * Ensure browser is ready and thread session exists.
781
+ * Creates a new page/context for the current thread if needed.
782
+ *
783
+ * For 'thread' scope, we need to create the thread session BEFORE
784
+ * calling super.ensureReady() because the base class's ensureReady() will
785
+ * call checkBrowserAlive(), which needs at least one thread browser to exist.
786
+ */
787
+ async ensureReady() {
788
+ const scope = this.threadManager.getScope();
789
+ const threadId = this.getCurrentThread();
790
+ const existingSession = this.threadManager.hasSession(threadId);
791
+ if (scope === "thread" && !existingSession) await this.getManagerForThread(threadId);
792
+ await super.ensureReady();
793
+ if (scope === "thread" && existingSession) await this.getManagerForThread(threadId);
794
+ }
795
+ /**
796
+ * Get the browser manager for the current thread.
797
+ * Delegates to ThreadManager for scope handling.
798
+ */
799
+ async getManagerForThread(threadId) {
800
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
801
+ if (this.threadManager.getScope() === "thread" && (!effectiveThreadId || effectiveThreadId === _mastra_core_browser.DEFAULT_THREAD_ID)) {
802
+ const existingManager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
803
+ if (existingManager) return existingManager;
804
+ }
805
+ return this.threadManager.getManagerForThread(effectiveThreadId);
806
+ }
807
+ async doLaunch() {
808
+ this.pendingCloseReasons.clear();
809
+ this.activeUrlChangeSources.clear();
810
+ if (this.threadManager.getScope() === "thread") {
811
+ this.sharedManager = new agent_browser.BrowserManager();
812
+ this.threadManager.setSharedManager(this.sharedManager);
813
+ return;
814
+ }
815
+ this.sharedManager = new agent_browser.BrowserManager();
816
+ const localConfig = this.config;
817
+ const launchOptions = {
818
+ headless: this.headless,
819
+ viewport: localConfig.viewport,
820
+ profile: localConfig.profile,
821
+ executablePath: localConfig.executablePath,
822
+ storageState: localConfig.storageState
823
+ };
824
+ if (localConfig.cdpUrl) launchOptions.cdpUrl = await this.resolveCdpUrl(localConfig.cdpUrl);
825
+ if (localConfig.cdpHeaders) launchOptions.cdpHeaders = localConfig.cdpHeaders;
826
+ await this.sharedManager.launch(launchOptions);
827
+ this.threadManager.setSharedManager(this.sharedManager);
828
+ this.setupCloseListenerForSharedScope(this.sharedManager);
829
+ }
830
+ /**
831
+ * Set up close event listeners for 'shared' scope browser.
832
+ * This handles the case where the shared browser is closed externally.
833
+ */
834
+ setupCloseListenerForSharedScope(manager) {
835
+ try {
836
+ const pidLookup = getBrowserPid(manager).then((pid) => {
837
+ if (pid && this.sharedManager === manager) this.sharedBrowserPid = pid;
838
+ }).finally(() => this.pidLookups.delete(pidLookup));
839
+ this.pidLookups.add(pidLookup);
840
+ let disconnectHandled = false;
841
+ const handleDisconnect = () => {
842
+ if (disconnectHandled) return;
843
+ disconnectHandled = true;
844
+ this.rememberClosedBrowserState(manager, "user");
845
+ pidLookup.catch(() => void 0).then(() => this.handleBrowserDisconnected());
846
+ };
847
+ const context = manager.getContext();
848
+ if (context) context.on("close", handleDisconnect);
849
+ const pages = manager.getPages();
850
+ for (const page of pages) page.on("close", () => {
851
+ if (manager.getPages().length === 0) handleDisconnect();
852
+ });
853
+ } catch {}
854
+ }
855
+ async doClose() {
856
+ await Promise.allSettled([...this.pidLookups]);
857
+ this.pidLookups.clear();
858
+ await this.threadManager.destroyAllSessions();
859
+ this.setCurrentThread(void 0);
860
+ if (this.threadManager.getScope() === "shared" && this.sharedManager) await this.sharedManager.close();
861
+ this.sharedManager = null;
862
+ }
863
+ async closeThreadSession(threadId) {
864
+ const manager = this.threadManager.getExistingManagerForThread(threadId);
865
+ if (manager) {
866
+ const state = this.getBrowserStateForManager(manager, threadId);
867
+ if (state) this.threadManager.updateBrowserState(threadId, state);
868
+ }
869
+ await super.closeThreadSession(threadId);
870
+ }
871
+ /**
872
+ * Check if the browser is still alive by verifying the page is connected.
873
+ * Called by base class ensureReady() to detect externally closed browsers.
874
+ */
875
+ async checkBrowserAlive() {
876
+ if (this.threadManager.getScope() === "thread") return this.threadManager.hasActiveThreadManagers();
877
+ if (!this.sharedManager) return false;
878
+ try {
879
+ const url = this.sharedManager.getPage().url();
880
+ if (url && url !== "about:blank") {
881
+ const state = await this.getBrowserState();
882
+ if (state) this.lastBrowserState = state;
883
+ }
884
+ return true;
885
+ } catch (error) {
886
+ const msg = error instanceof Error ? error.message : String(error);
887
+ if (this.isDisconnectionError(msg)) this.logger.debug?.("Browser was externally closed");
888
+ return false;
889
+ }
890
+ }
891
+ /**
892
+ * Get the browser tools for this provider.
893
+ * Returns 16 flat tools for browser automation.
894
+ */
895
+ getTools() {
896
+ const tools = createAgentBrowserTools(this);
897
+ if (this.browserConfig.recording) Object.assign(tools, (0, _mastra_core_browser.createBrowserRecordingTools)(this, this.browserConfig.recording));
898
+ const exclude = this.browserConfig.excludeTools;
899
+ if (exclude?.length) for (const name of exclude) delete tools[name];
900
+ return tools;
901
+ }
902
+ browserStateKey(threadId) {
903
+ return threadId ?? this.getCurrentThread() ?? _mastra_core_browser.DEFAULT_THREAD_ID;
904
+ }
905
+ markBrowserCloseReason(reason, threadId) {
906
+ this.pendingCloseReasons.set(this.browserStateKey(threadId), reason);
907
+ }
908
+ markActiveUrlChangeSource(source, url, threadId) {
909
+ this.activeUrlChangeSources.set(this.browserStateKey(threadId), {
910
+ url,
911
+ source
912
+ });
913
+ }
914
+ getCloseReason(threadId) {
915
+ return this.pendingCloseReasons.get(this.browserStateKey(threadId)) ?? this.pendingCloseReasons.get(_mastra_core_browser.DEFAULT_THREAD_ID);
916
+ }
917
+ getActiveUrlChangeSource(activeUrl, threadId) {
918
+ const entry = this.activeUrlChangeSources.get(this.browserStateKey(threadId));
919
+ return entry && entry.url === activeUrl ? entry.source : void 0;
920
+ }
921
+ rememberClosedBrowserState(manager, reason, threadId) {
922
+ const state = this.getBrowserStateForManager(manager, threadId);
923
+ if (!state || state.tabs.length === 0) return;
924
+ const closedState = {
925
+ ...state,
926
+ closeReason: this.getCloseReason(threadId) ?? reason
927
+ };
928
+ if (threadId) this.threadManager.updateBrowserState(threadId, closedState);
929
+ else this.lastBrowserState = closedState;
930
+ }
931
+ /**
932
+ * Get the page for the current thread.
933
+ * Uses thread scope if enabled, otherwise returns the shared page.
934
+ * @param explicitThreadId - Optional thread ID to use instead of getCurrentThread()
935
+ * Use this to avoid race conditions in concurrent tool calls.
936
+ */
937
+ async getPage(explicitThreadId) {
938
+ const scope = this.getScope();
939
+ const threadId = explicitThreadId ?? this.getCurrentThread();
940
+ if (scope === "thread") return this.threadManager.getPageForThread(threadId);
941
+ if (!this.sharedManager) throw new Error("Browser not launched");
942
+ return this.sharedManager.getPage();
943
+ }
944
+ /**
945
+ * Get the active page for a thread (implements abstract method from base class).
946
+ * Returns null if no page is available, unlike getPage which throws.
947
+ */
948
+ async getActivePage(threadId) {
949
+ try {
950
+ return await this.getPage(threadId);
951
+ } catch {
952
+ return null;
953
+ }
954
+ }
955
+ /**
956
+ * Set up close event listener for a thread's browser manager.
957
+ * This handles the case where a thread's browser is closed externally.
958
+ */
959
+ setupCloseListenerForThread(manager, threadId) {
960
+ try {
961
+ const pidLookup = getBrowserPid(manager).then((pid) => {
962
+ if (pid && this.threadManager?.getExistingManagerForThread(threadId) === manager) this.threadBrowserPids.set(threadId, pid);
963
+ }).finally(() => this.pidLookups.delete(pidLookup));
964
+ this.pidLookups.add(pidLookup);
965
+ let disconnectHandled = false;
966
+ const handleDisconnect = () => {
967
+ if (disconnectHandled) return;
968
+ disconnectHandled = true;
969
+ this.rememberClosedBrowserState(manager, "user", threadId);
970
+ pidLookup.catch(() => void 0).then(() => this.handleThreadBrowserDisconnected(threadId));
971
+ };
972
+ const context = manager.getContext();
973
+ if (context) context.on("close", handleDisconnect);
974
+ const pages = manager.getPages();
975
+ for (const page of pages) page.on("close", () => {
976
+ if (manager.getPages().length === 0) handleDisconnect();
977
+ });
978
+ } catch {}
979
+ }
980
+ /**
981
+ * Create an error response from an exception.
982
+ * Extends base class to add agent-browser specific error handling.
983
+ */
984
+ createErrorFromException(error, context) {
985
+ const msg = error instanceof Error ? error.message : String(error);
986
+ if (msg.includes("stale") || msg.includes("Stale")) return this.createError("stale_ref", "Element ref is no longer valid.", "Get a fresh snapshot and use updated refs.");
987
+ if (msg.includes("not found") || msg.includes("No element")) return this.createError("element_not_found", "Element not found.", "Check the ref is correct or get a fresh snapshot.");
988
+ return super.createErrorFromException(error, context);
989
+ }
990
+ async requireLocator(ref, threadId) {
991
+ return (await this.getManagerForThread(threadId)).getLocatorFromRef(ref);
992
+ }
993
+ async getScrollInfo(threadId) {
994
+ const info = await (await this.getPage(threadId)).evaluate(`({
894
995
  scrollY: Math.round(window.scrollY),
895
996
  scrollHeight: document.documentElement.scrollHeight,
896
997
  viewportHeight: window.innerHeight
897
998
  })`);
898
- if (!info || typeof info.scrollHeight !== "number") {
899
- return {
900
- scrollY: 0,
901
- scrollHeight: 0,
902
- viewportHeight: 0,
903
- atTop: true,
904
- atBottom: true,
905
- percentDown: 0
906
- };
907
- }
908
- const maxScroll = info.scrollHeight - info.viewportHeight;
909
- return {
910
- ...info,
911
- atTop: info.scrollY < 50,
912
- atBottom: info.scrollY >= maxScroll - 50,
913
- percentDown: maxScroll > 0 ? Math.round(info.scrollY / maxScroll * 100) : 0
914
- };
915
- }
916
- // ---------------------------------------------------------------------------
917
- // URL Access
918
- // ---------------------------------------------------------------------------
919
- /**
920
- * Get the current page URL without launching the browser.
921
- * @param threadId - Optional thread ID for thread-isolated browsers
922
- * @returns The current URL string, or null if browser is not running
923
- */
924
- async getCurrentUrl(threadId) {
925
- if (!this.isBrowserRunning()) {
926
- return null;
927
- }
928
- try {
929
- const effectiveThreadId = threadId ?? this.getCurrentThread();
930
- const scope = this.threadManager.getScope();
931
- if (scope === "thread" && effectiveThreadId) {
932
- const manager2 = this.threadManager.getExistingManagerForThread(effectiveThreadId);
933
- if (!manager2) {
934
- return null;
935
- }
936
- const url2 = manager2.getPage().url();
937
- if (url2 && url2 !== "about:blank") {
938
- const state = this.getBrowserStateForManager(manager2);
939
- if (state) {
940
- this.threadManager.updateBrowserState(effectiveThreadId, state);
941
- }
942
- }
943
- return url2;
944
- }
945
- const manager = await this.getManagerForThread(threadId);
946
- const url = manager.getPage().url();
947
- if (url && url !== "about:blank") {
948
- const state = this.getBrowserStateForManager(manager);
949
- if (state) {
950
- this.lastBrowserState = state;
951
- }
952
- }
953
- return url;
954
- } catch {
955
- return null;
956
- }
957
- }
958
- /**
959
- * Navigate to a URL (simple form). Used internally for restoring state on relaunch.
960
- */
961
- async navigateTo(url) {
962
- if (!this.isBrowserRunning()) {
963
- return;
964
- }
965
- try {
966
- const page = await this.getPage();
967
- await page.goto(url, {
968
- timeout: this.defaultTimeout,
969
- waitUntil: "domcontentloaded"
970
- });
971
- } catch {
972
- }
973
- }
974
- /**
975
- * Get the current browser state (all tabs and active tab index).
976
- */
977
- async getBrowserState(threadId) {
978
- if (!this.isBrowserRunning(threadId)) {
979
- return null;
980
- }
981
- try {
982
- const manager = await this.getManagerForThread(threadId);
983
- return this.getBrowserStateForManager(manager, threadId);
984
- } catch {
985
- return null;
986
- }
987
- }
988
- /**
989
- * Get browser state for a thread (implements abstract method from base class).
990
- * Sync version that uses existing manager lookup without creating sessions.
991
- */
992
- getBrowserStateForThread(threadId) {
993
- const effectiveThreadId = threadId ?? this.getCurrentThread() ?? browser.DEFAULT_THREAD_ID;
994
- const manager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
995
- if (!manager) return null;
996
- return this.getBrowserStateForManager(manager, effectiveThreadId);
997
- }
998
- /**
999
- * Get browser state from a specific manager instance.
1000
- */
1001
- getBrowserStateForManager(manager, threadId) {
1002
- try {
1003
- const stateKey = this.browserStateKey(threadId);
1004
- const pages = manager.getPages();
1005
- const activeIndex = manager.getActiveIndex();
1006
- const tabs = pages.map((page) => ({
1007
- url: page.url()
1008
- }));
1009
- const activeUrl = tabs[activeIndex]?.url;
1010
- const previousState = this.threadManager.getSavedBrowserState(stateKey) ?? this.lastBrowserState;
1011
- const previousUrl = previousState?.tabs[previousState.activeTabIndex]?.url;
1012
- const activeUrlChangeSource = this.getActiveUrlChangeSource(activeUrl, stateKey) ?? (previousUrl && activeUrl !== previousUrl ? "user" : void 0);
1013
- const state = {
1014
- tabs,
1015
- activeTabIndex: activeIndex,
1016
- ...this.getCloseReason(stateKey) ? { closeReason: this.getCloseReason(stateKey) } : {},
1017
- ...activeUrlChangeSource ? { activeUrlChangeSource } : {}
1018
- };
1019
- this.threadManager.updateBrowserState(stateKey, state);
1020
- this.lastBrowserState = state;
1021
- return state;
1022
- } catch {
1023
- return null;
1024
- }
1025
- }
1026
- /**
1027
- * Get all open tabs with their URLs and titles.
1028
- */
1029
- async getTabState(threadId) {
1030
- const state = await this.getBrowserState(threadId);
1031
- return state?.tabs ?? [];
1032
- }
1033
- /**
1034
- * Get the active tab index.
1035
- */
1036
- async getActiveTabIndex(threadId) {
1037
- if (!this.isBrowserRunning()) {
1038
- return 0;
1039
- }
1040
- try {
1041
- const manager = await this.getManagerForThread(threadId);
1042
- return manager.getActiveIndex();
1043
- } catch {
1044
- return 0;
1045
- }
1046
- }
1047
- /**
1048
- * Export the current browser session's storage state (cookies, localStorage) to a JSON file.
1049
- * This can later be loaded via the `storageState` config option to restore the session.
1050
- *
1051
- * @param path - File path to save the storage state JSON
1052
- * @param threadId - Optional thread ID (defaults to current thread)
1053
- */
1054
- async exportStorageState(path, threadId) {
1055
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1056
- const manager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1057
- if (!manager) {
1058
- throw new Error("No browser is running. Launch a browser first before exporting storage state.");
1059
- }
1060
- const context = manager.getContext();
1061
- if (!context) {
1062
- throw new Error("Browser context not available");
1063
- }
1064
- await context.storageState({ path });
1065
- }
1066
- // ---------------------------------------------------------------------------
1067
- // 1. browser_goto - Navigate to URL
1068
- // ---------------------------------------------------------------------------
1069
- async goto(input, threadId) {
1070
- try {
1071
- const page = await this.getPage(threadId);
1072
- await page.goto(input.url, {
1073
- timeout: input.timeout ?? this.defaultTimeout,
1074
- waitUntil: input.waitUntil ?? "domcontentloaded"
1075
- });
1076
- const url = page.url();
1077
- this.markActiveUrlChangeSource("agent", url, threadId);
1078
- return {
1079
- success: true,
1080
- url,
1081
- title: await page.title(),
1082
- hint: "Take a snapshot to see interactive elements and get refs."
1083
- };
1084
- } catch (error) {
1085
- return this.createErrorFromException(error, "Goto");
1086
- }
1087
- }
1088
- // ---------------------------------------------------------------------------
1089
- // 2. browser_snapshot - Capture accessibility tree
1090
- // ---------------------------------------------------------------------------
1091
- async snapshot(input, threadId) {
1092
- try {
1093
- const manager = await this.getManagerForThread(threadId);
1094
- const page = await this.getPage(threadId);
1095
- const rawSnapshot = await manager.getSnapshot({
1096
- interactive: input.interactiveOnly ?? true,
1097
- compact: true
1098
- });
1099
- const snapshot = (rawSnapshot.tree ?? "").replace(/\[ref=(\w+)\]/g, "@$1");
1100
- const scrollInfo = await this.getScrollInfo(threadId);
1101
- let scrollText;
1102
- if (scrollInfo.atTop && !scrollInfo.atBottom) {
1103
- scrollText = "TOP - more content below";
1104
- } else if (scrollInfo.atBottom) {
1105
- scrollText = "BOTTOM of page";
1106
- } else {
1107
- scrollText = `${scrollInfo.percentDown}% down`;
1108
- }
1109
- const refs = snapshot.match(/@e\d+/g) || [];
1110
- const elementCount = new Set(refs).size;
1111
- return {
1112
- success: true,
1113
- snapshot,
1114
- url: page.url(),
1115
- title: await page.title(),
1116
- elementCount,
1117
- scroll: scrollText,
1118
- hint: elementCount === 0 ? "No interactive elements found. Try scrolling or setting interactiveOnly:false." : void 0
1119
- };
1120
- } catch (error) {
1121
- return this.createErrorFromException(error, "Snapshot");
1122
- }
1123
- }
1124
- // ---------------------------------------------------------------------------
1125
- // browser_screenshot - Capture a screenshot of the current page
1126
- // ---------------------------------------------------------------------------
1127
- async screenshot(input, threadId) {
1128
- try {
1129
- const page = await this.getPage(threadId);
1130
- const buffer = await page.screenshot({
1131
- fullPage: input.fullPage ?? false,
1132
- type: "png"
1133
- });
1134
- const base64 = Buffer.from(buffer).toString("base64");
1135
- return {
1136
- base64,
1137
- url: page.url(),
1138
- title: await page.title()
1139
- };
1140
- } catch (error) {
1141
- return this.createErrorFromException(error, "Screenshot");
1142
- }
1143
- }
1144
- /**
1145
- * Start a `waitForNavigation` wait (when `waitUntil` is set) and immediately
1146
- * attach a noop catch handler so a navigation timeout/rejection can't become
1147
- * an unhandled rejection (crashing the process) while the caller's action is
1148
- * still pending. Callers should still `await` the returned promise to observe
1149
- * the original error.
1150
- */
1151
- startNavigationWait(page, waitUntil, timeout) {
1152
- const navigation = waitUntil ? page.waitForNavigation({ waitUntil, timeout }) : void 0;
1153
- navigation?.catch(() => {
1154
- });
1155
- return navigation;
1156
- }
1157
- // ---------------------------------------------------------------------------
1158
- // 3. browser_click - Click on element
1159
- // ---------------------------------------------------------------------------
1160
- async click(input, threadId) {
1161
- try {
1162
- const page = await this.getPage(threadId);
1163
- const locator = await this.requireLocator(input.ref, threadId);
1164
- if (!locator) {
1165
- return this.createError(
1166
- "stale_ref",
1167
- `Ref ${input.ref} not found. The page has changed.`,
1168
- "Take a new snapshot to see the current page state and get fresh refs."
1169
- );
1170
- }
1171
- const timeout = input.timeout ?? this.defaultTimeout;
1172
- const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1173
- await locator.click({
1174
- button: input.button ?? "left",
1175
- clickCount: input.clickCount ?? 1,
1176
- modifiers: input.modifiers,
1177
- timeout
1178
- });
1179
- await navigation;
1180
- return {
1181
- success: true,
1182
- url: page.url(),
1183
- hint: "Take a new snapshot to see updated page state and get fresh refs."
1184
- };
1185
- } catch (error) {
1186
- const errorMsg = error instanceof Error ? error.message : String(error);
1187
- if (errorMsg.includes("intercepts pointer events")) {
1188
- return this.createError(
1189
- "element_blocked",
1190
- `Element ${input.ref} is blocked by another element.`,
1191
- "Take a new snapshot to see what is blocking. Dismiss any modals or scroll the element into view."
1192
- );
1193
- }
1194
- return this.createErrorFromException(error, "Click");
1195
- }
1196
- }
1197
- // ---------------------------------------------------------------------------
1198
- // 4. browser_type - Type text into element
1199
- // ---------------------------------------------------------------------------
1200
- async type(input, threadId) {
1201
- try {
1202
- const page = await this.getPage(threadId);
1203
- const locator = await this.requireLocator(input.ref, threadId);
1204
- if (!locator) {
1205
- return this.createError(
1206
- "stale_ref",
1207
- `Ref ${input.ref} not found. The page has changed.`,
1208
- "Take a new snapshot to see the current page state and get fresh refs."
1209
- );
1210
- }
1211
- if (input.clear) {
1212
- await locator.fill("", { timeout: this.defaultTimeout });
1213
- }
1214
- if (input.delay) {
1215
- await locator.focus();
1216
- for (const char of input.text) {
1217
- await page.keyboard.press(char);
1218
- await new Promise((r) => setTimeout(r, input.delay));
1219
- }
1220
- } else {
1221
- await locator.fill(input.text, { timeout: this.defaultTimeout });
1222
- }
1223
- const value = await locator.inputValue({ timeout: 1e3 }).catch(() => input.text);
1224
- return {
1225
- success: true,
1226
- value,
1227
- url: page.url(),
1228
- hint: "Take a new snapshot if you need to interact with more elements."
1229
- };
1230
- } catch (error) {
1231
- const errorMsg = error instanceof Error ? error.message : String(error);
1232
- if (errorMsg.includes("is not an <input>") || errorMsg.includes("not an input") || errorMsg.includes("Cannot type") || errorMsg.includes("not focusable")) {
1233
- return this.createError(
1234
- "not_focusable",
1235
- `Element ${input.ref} is not a text input field.`,
1236
- 'Take a new snapshot and look for elements with role "textbox" or "searchbox".'
1237
- );
1238
- }
1239
- return this.createErrorFromException(error, "Type");
1240
- }
1241
- }
1242
- // ---------------------------------------------------------------------------
1243
- // 5. browser_press - Press keyboard key(s)
1244
- // ---------------------------------------------------------------------------
1245
- async press(input, threadId) {
1246
- try {
1247
- const page = await this.getPage(threadId);
1248
- const timeout = input.timeout ?? this.defaultTimeout;
1249
- const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1250
- await page.keyboard.press(input.key);
1251
- await navigation;
1252
- return {
1253
- success: true,
1254
- url: page.url(),
1255
- hint: "Take a new snapshot if the page may have changed."
1256
- };
1257
- } catch (error) {
1258
- return this.createErrorFromException(error, "Press");
1259
- }
1260
- }
1261
- // ---------------------------------------------------------------------------
1262
- // 6. browser_select - Select dropdown option
1263
- // ---------------------------------------------------------------------------
1264
- async select(input, threadId) {
1265
- try {
1266
- const page = await this.getPage(threadId);
1267
- const locator = await this.requireLocator(input.ref, threadId);
1268
- if (!locator) {
1269
- return this.createError(
1270
- "stale_ref",
1271
- `Ref ${input.ref} not found. The page has changed.`,
1272
- "Take a new snapshot to get fresh refs."
1273
- );
1274
- }
1275
- const selectValue = {};
1276
- if (input.value) selectValue.value = input.value;
1277
- if (input.label) selectValue.label = input.label;
1278
- if (input.index !== void 0) selectValue.index = input.index;
1279
- const timeout = input.timeout ?? this.defaultTimeout;
1280
- const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1281
- const selected = await locator.selectOption(selectValue, { timeout });
1282
- await navigation;
1283
- return {
1284
- success: true,
1285
- selected,
1286
- url: page.url(),
1287
- hint: "Selection complete. Take a snapshot if you need to continue."
1288
- };
1289
- } catch (error) {
1290
- return this.createErrorFromException(error, "Select");
1291
- }
1292
- }
1293
- // ---------------------------------------------------------------------------
1294
- // 7. browser_scroll - Scroll page or element
1295
- // ---------------------------------------------------------------------------
1296
- async scroll(input, threadId) {
1297
- try {
1298
- const page = await this.getPage(threadId);
1299
- if (input.ref) {
1300
- const locator = await this.requireLocator(input.ref, threadId);
1301
- if (locator) {
1302
- await locator.scrollIntoViewIfNeeded({ timeout: this.defaultTimeout });
1303
- }
1304
- } else {
1305
- const direction = input.direction;
1306
- const amount = input.amount ?? 300;
1307
- let deltaX = 0;
1308
- let deltaY = 0;
1309
- switch (direction) {
1310
- case "up":
1311
- deltaY = -amount;
1312
- break;
1313
- case "down":
1314
- deltaY = amount;
1315
- break;
1316
- case "left":
1317
- deltaX = -amount;
1318
- break;
1319
- case "right":
1320
- deltaX = amount;
1321
- break;
1322
- }
1323
- await page.evaluate(
1324
- ({ x, y }) => {
1325
- globalThis.scrollBy(x, y);
1326
- },
1327
- { x: deltaX, y: deltaY }
1328
- );
1329
- }
1330
- const scrollInfo = await this.getScrollInfo(threadId);
1331
- let scrollText;
1332
- if (scrollInfo.atTop && !scrollInfo.atBottom) {
1333
- scrollText = "TOP - more content below";
1334
- } else if (scrollInfo.atBottom) {
1335
- scrollText = "BOTTOM of page";
1336
- } else {
1337
- scrollText = `${scrollInfo.percentDown}% down`;
1338
- }
1339
- return {
1340
- success: true,
1341
- position: { x: 0, y: scrollInfo.scrollY },
1342
- scroll: scrollText,
1343
- hint: "Take a new snapshot to see elements in the new viewport."
1344
- };
1345
- } catch (error) {
1346
- return this.createErrorFromException(error, "Scroll");
1347
- }
1348
- }
1349
- // ---------------------------------------------------------------------------
1350
- // 8. browser_hover - Hover over element
1351
- // ---------------------------------------------------------------------------
1352
- async hover(input, threadId) {
1353
- try {
1354
- const page = await this.getPage(threadId);
1355
- const locator = await this.requireLocator(input.ref, threadId);
1356
- if (!locator) {
1357
- return this.createError(
1358
- "stale_ref",
1359
- `Ref ${input.ref} not found. The page has changed.`,
1360
- "Take a new snapshot to get fresh refs."
1361
- );
1362
- }
1363
- await locator.hover({ timeout: this.defaultTimeout });
1364
- return {
1365
- success: true,
1366
- url: page.url(),
1367
- hint: "Take a new snapshot to see any hover-triggered elements (dropdowns, tooltips)."
1368
- };
1369
- } catch (error) {
1370
- return this.createErrorFromException(error, "Hover");
1371
- }
1372
- }
1373
- // ---------------------------------------------------------------------------
1374
- // 10. browser_back - Navigate back
1375
- // ---------------------------------------------------------------------------
1376
- async back(threadId) {
1377
- try {
1378
- const page = await this.getPage(threadId);
1379
- await page.goBack({ timeout: this.defaultTimeout });
1380
- const url = page.url();
1381
- this.markActiveUrlChangeSource("agent", url, threadId);
1382
- return {
1383
- success: true,
1384
- url,
1385
- title: await page.title(),
1386
- hint: "Take a new snapshot to see the previous page."
1387
- };
1388
- } catch (error) {
1389
- return this.createErrorFromException(error, "Back");
1390
- }
1391
- }
1392
- // ---------------------------------------------------------------------------
1393
- // 11. browser_dialog - Click element that triggers dialog and handle it
1394
- // ---------------------------------------------------------------------------
1395
- async dialog(input, threadId) {
1396
- try {
1397
- const page = await this.getPage(threadId);
1398
- const locator = await this.requireLocator(input.triggerRef, threadId);
1399
- if (!locator) {
1400
- return this.createError(
1401
- "stale_ref",
1402
- `Trigger ref ${input.triggerRef} not found.`,
1403
- "Take a new snapshot to get fresh refs."
1404
- );
1405
- }
1406
- return new Promise((resolve, reject) => {
1407
- const timeout = setTimeout(() => {
1408
- page.off("dialog", dialogHandler);
1409
- reject(
1410
- new Error(`No dialog appeared after clicking ${input.triggerRef}. The element may not trigger a dialog.`)
1411
- );
1412
- }, this.defaultTimeout);
1413
- const dialogHandler = async (dialog) => {
1414
- clearTimeout(timeout);
1415
- try {
1416
- const dialogType = dialog.type();
1417
- const message = dialog.message();
1418
- if (input.action === "accept") {
1419
- await dialog.accept(input.text);
1420
- } else {
1421
- await dialog.dismiss();
1422
- }
1423
- resolve({
1424
- success: true,
1425
- action: input.action,
1426
- dialogType,
1427
- message,
1428
- hint: "Dialog handled. Take a snapshot to continue."
1429
- });
1430
- } catch (e) {
1431
- reject(e);
1432
- }
1433
- };
1434
- page.once("dialog", dialogHandler);
1435
- locator.click({ timeout: this.defaultTimeout }).catch((e) => {
1436
- clearTimeout(timeout);
1437
- page.off("dialog", dialogHandler);
1438
- reject(e);
1439
- });
1440
- });
1441
- } catch (error) {
1442
- return this.createErrorFromException(error, "Dialog");
1443
- }
1444
- }
1445
- // ---------------------------------------------------------------------------
1446
- // 13. browser_wait - Wait for element or condition
1447
- // ---------------------------------------------------------------------------
1448
- async wait(input, threadId) {
1449
- try {
1450
- const timeout = input.timeout ?? this.defaultTimeout;
1451
- if (input.ref) {
1452
- const locator = await this.requireLocator(input.ref, threadId);
1453
- if (!locator) {
1454
- return this.createError("stale_ref", `Ref ${input.ref} not found.`, "Take a new snapshot to get fresh refs.");
1455
- }
1456
- const state = input.state ?? "visible";
1457
- await locator.waitFor({ state, timeout });
1458
- return {
1459
- success: true,
1460
- hint: `Element is now ${state}. Take a snapshot to continue.`
1461
- };
1462
- } else {
1463
- const page = await this.getPage(threadId);
1464
- await page.waitForTimeout(timeout);
1465
- return {
1466
- success: true,
1467
- hint: "Wait complete. Take a snapshot to see current state."
1468
- };
1469
- }
1470
- } catch (error) {
1471
- return this.createErrorFromException(error, "Wait");
1472
- }
1473
- }
1474
- // ---------------------------------------------------------------------------
1475
- // 14. browser_tabs - Manage browser tabs
1476
- // ---------------------------------------------------------------------------
1477
- async tabs(input, threadId) {
1478
- try {
1479
- const browser = await this.getManagerForThread(threadId);
1480
- if (!browser) {
1481
- return this.createError(
1482
- "browser_closed",
1483
- "Browser not launched",
1484
- "Call a navigation tool first to launch the browser."
1485
- );
1486
- }
1487
- switch (input.action) {
1488
- case "list": {
1489
- if (!browser.listTabs) {
1490
- return this.createError(
1491
- "browser_error",
1492
- "Tab management not supported",
1493
- "This browser provider does not support tab management."
1494
- );
1495
- }
1496
- const tabsList = await browser.listTabs();
1497
- return {
1498
- success: true,
1499
- tabs: tabsList,
1500
- hint: 'Use browser_tabs with action:"switch" and index to change tabs.'
1501
- };
1502
- }
1503
- case "new": {
1504
- if (!browser.newTab) {
1505
- return this.createError(
1506
- "browser_error",
1507
- "Tab management not supported",
1508
- "This browser provider does not support tab management."
1509
- );
1510
- }
1511
- const result = await browser.newTab();
1512
- if (input.url) {
1513
- const page = await this.getPage(threadId);
1514
- await page.goto(input.url);
1515
- this.markActiveUrlChangeSource("agent", page.url(), threadId);
1516
- }
1517
- this.updateSessionBrowserState(threadId);
1518
- return {
1519
- success: true,
1520
- ...result,
1521
- hint: "New tab opened. Take a snapshot to see its content."
1522
- };
1523
- }
1524
- case "switch": {
1525
- if (!browser.switchTo) {
1526
- return this.createError(
1527
- "browser_error",
1528
- "Tab management not supported",
1529
- "This browser provider does not support tab management."
1530
- );
1531
- }
1532
- await browser.switchTo(input.index);
1533
- await this.reconnectScreencastForThread(threadId, "tab switch");
1534
- const page = browser.getPage();
1535
- const pageUrl = page.url();
1536
- this.markActiveUrlChangeSource("agent", pageUrl, threadId);
1537
- const streamKey = this.getStreamKey(threadId);
1538
- const stream = this.activeScreencastStreams.get(streamKey);
1539
- if (pageUrl && stream?.isActive()) {
1540
- stream.emitUrl(pageUrl);
1541
- }
1542
- this.updateSessionBrowserState(threadId);
1543
- return {
1544
- success: true,
1545
- index: input.index,
1546
- url: pageUrl,
1547
- title: await page.title(),
1548
- hint: "Tab switched. Take a snapshot to see its content."
1549
- };
1550
- }
1551
- case "close": {
1552
- if (!browser.closeTab) {
1553
- return this.createError(
1554
- "browser_error",
1555
- "Tab management not supported",
1556
- "This browser provider does not support tab management."
1557
- );
1558
- }
1559
- await browser.closeTab(input.index);
1560
- await this.reconnectScreencastForThread(threadId, "tab close");
1561
- this.updateSessionBrowserState(threadId);
1562
- const tabsList = await browser.listTabs?.() ?? [];
1563
- return {
1564
- success: true,
1565
- remaining: tabsList.length,
1566
- hint: tabsList.length > 0 ? "Tab closed. Take a snapshot to see current tab." : "All tabs closed."
1567
- };
1568
- }
1569
- default:
1570
- return this.createError(
1571
- "browser_error",
1572
- `Unknown tabs action: ${input.action}`,
1573
- 'Use "list", "new", "switch", or "close".'
1574
- );
1575
- }
1576
- } catch (error) {
1577
- return this.createErrorFromException(error, "Tabs");
1578
- }
1579
- }
1580
- // ---------------------------------------------------------------------------
1581
- // 15. browser_drag - Drag element to target
1582
- // ---------------------------------------------------------------------------
1583
- async drag(input, threadId) {
1584
- try {
1585
- const page = await this.getPage(threadId);
1586
- let sourceLocator = null;
1587
- if (input.sourceRef) {
1588
- sourceLocator = await this.requireLocator(input.sourceRef, threadId);
1589
- } else if (input.sourceSelector) {
1590
- sourceLocator = page.locator(input.sourceSelector);
1591
- }
1592
- if (!sourceLocator) {
1593
- return this.createError(
1594
- "stale_ref",
1595
- input.sourceRef ? `Source ref ${input.sourceRef} not found.` : "No source element specified. Provide sourceRef or sourceSelector.",
1596
- input.sourceRef ? "Take a new snapshot to get fresh refs, or use sourceSelector for elements not in the accessibility tree." : void 0
1597
- );
1598
- }
1599
- let targetLocator = null;
1600
- if (input.targetRef) {
1601
- targetLocator = await this.requireLocator(input.targetRef, threadId);
1602
- } else if (input.targetSelector) {
1603
- targetLocator = page.locator(input.targetSelector);
1604
- }
1605
- if (!targetLocator) {
1606
- return this.createError(
1607
- "stale_ref",
1608
- input.targetRef ? `Target ref ${input.targetRef} not found.` : "No target element specified. Provide targetRef or targetSelector.",
1609
- input.targetRef ? "Take a new snapshot to get fresh refs, or use targetSelector for elements not in the accessibility tree." : void 0
1610
- );
1611
- }
1612
- await sourceLocator.dragTo(targetLocator, { timeout: this.defaultTimeout });
1613
- return {
1614
- success: true,
1615
- url: page.url(),
1616
- hint: "Drag complete. Take a snapshot to see the result."
1617
- };
1618
- } catch (error) {
1619
- return this.createErrorFromException(error, "Drag");
1620
- }
1621
- }
1622
- // ---------------------------------------------------------------------------
1623
- // 16. browser_evaluate - Execute JavaScript
1624
- // ---------------------------------------------------------------------------
1625
- async evaluate(input, threadId) {
1626
- try {
1627
- const page = await this.getPage(threadId);
1628
- const result = await page.evaluate(input.script);
1629
- return {
1630
- success: true,
1631
- result,
1632
- hint: "JavaScript executed. Take a snapshot if the page may have changed."
1633
- };
1634
- } catch (error) {
1635
- return this.createErrorFromException(error, "Evaluate");
1636
- }
1637
- }
1638
- // ---------------------------------------------------------------------------
1639
- // 17. browser_close - Close browser
1640
- // ---------------------------------------------------------------------------
1641
- async closeBrowser() {
1642
- try {
1643
- await this.close();
1644
- return {
1645
- success: true,
1646
- hint: "Browser closed. Call browser_goto to start a new session."
1647
- };
1648
- } catch (error) {
1649
- return this.createErrorFromException(error, "Close");
1650
- }
1651
- }
1652
- // ---------------------------------------------------------------------------
1653
- // Screencast (for Studio live view)
1654
- // ---------------------------------------------------------------------------
1655
- async startScreencast(_options) {
1656
- const requestedThreadId = _options?.threadId;
1657
- const effectiveThreadId = this.getScope() === "thread" ? requestedThreadId ?? this.getCurrentThread() ?? browser.DEFAULT_THREAD_ID : requestedThreadId;
1658
- let browserManager;
1659
- if (this.getScope() === "thread") {
1660
- browserManager = await this.getManagerForThread(effectiveThreadId);
1661
- } else {
1662
- if (!this.sharedManager) throw new Error("Browser not launched");
1663
- browserManager = this.sharedManager;
1664
- }
1665
- const provider = {
1666
- getCdpSession: async () => {
1667
- const currentPage = browserManager.getPage();
1668
- if (!currentPage) {
1669
- throw new Error("No active page available");
1670
- }
1671
- const cdpSession = await currentPage.context().newCDPSession(currentPage);
1672
- return cdpSession;
1673
- },
1674
- isBrowserRunning: () => browserManager.isLaunched()
1675
- };
1676
- const stream = new browser.ScreencastStreamImpl(provider, _options);
1677
- const streamKey = this.getStreamKey(effectiveThreadId);
1678
- this.activeScreencastStreams.set(streamKey, stream);
1679
- const context = browserManager.getContext();
1680
- if (context) {
1681
- const onNewPage = (_newPage) => {
1682
- setTimeout(() => {
1683
- if (stream.isActive()) {
1684
- stream.reconnect().catch(() => {
1685
- });
1686
- }
1687
- }, 100);
1688
- };
1689
- context.on("page", onNewPage);
1690
- const pageCloseHandlers = /* @__PURE__ */ new Map();
1691
- const frameNavigatedHandlers = /* @__PURE__ */ new Map();
1692
- const setupPageListeners = (page) => {
1693
- const onFrameNavigated = (frame) => {
1694
- if (!frame.parentFrame()) {
1695
- stream.emitUrl(frame.url());
1696
- this.updateSessionBrowserState(effectiveThreadId);
1697
- }
1698
- };
1699
- page.on("framenavigated", onFrameNavigated);
1700
- frameNavigatedHandlers.set(page, onFrameNavigated);
1701
- const onClose = () => {
1702
- pageCloseHandlers.delete(page);
1703
- const navHandler = frameNavigatedHandlers.get(page);
1704
- if (navHandler) {
1705
- page.off("framenavigated", navHandler);
1706
- frameNavigatedHandlers.delete(page);
1707
- }
1708
- setTimeout(() => {
1709
- const remainingPages = browserManager.getPages();
1710
- if (stream.isActive() && remainingPages.length > 0) {
1711
- stream.reconnect().catch(() => {
1712
- });
1713
- const activePage = remainingPages[browserManager.getActiveIndex()] || remainingPages[0];
1714
- if (activePage) {
1715
- const url = activePage.url();
1716
- if (url && url !== "about:blank") {
1717
- stream.emitUrl(url);
1718
- }
1719
- }
1720
- }
1721
- }, 100);
1722
- };
1723
- page.once("close", onClose);
1724
- pageCloseHandlers.set(page, onClose);
1725
- };
1726
- const setupPageCloseListener = setupPageListeners;
1727
- for (const page of browserManager.getPages()) {
1728
- setupPageCloseListener(page);
1729
- }
1730
- const onNewPageWithCloseListener = (newPage) => {
1731
- setupPageCloseListener(newPage);
1732
- const url = newPage.url();
1733
- if (url && url !== "about:blank") {
1734
- stream.emitUrl(url);
1735
- }
1736
- onNewPage();
1737
- };
1738
- context.off("page", onNewPage);
1739
- context.on("page", onNewPageWithCloseListener);
1740
- stream.once("stop", () => {
1741
- context.off("page", onNewPageWithCloseListener);
1742
- for (const [page, handler] of pageCloseHandlers) {
1743
- page.off("close", handler);
1744
- }
1745
- pageCloseHandlers.clear();
1746
- for (const [page, handler] of frameNavigatedHandlers) {
1747
- page.off("framenavigated", handler);
1748
- }
1749
- frameNavigatedHandlers.clear();
1750
- this.activeScreencastStreams.delete(streamKey);
1751
- });
1752
- }
1753
- await stream.start();
1754
- return stream;
1755
- }
1756
- // ---------------------------------------------------------------------------
1757
- // Event Injection (for Studio live view interactivity)
1758
- // ---------------------------------------------------------------------------
1759
- async injectMouseEvent(event, threadId) {
1760
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1761
- const manager = await this.getManagerForThread(effectiveThreadId);
1762
- await manager.injectMouseEvent(event);
1763
- }
1764
- async injectKeyboardEvent(event, threadId) {
1765
- const effectiveThreadId = threadId ?? this.getCurrentThread();
1766
- const manager = await this.getManagerForThread(effectiveThreadId);
1767
- const cdp = await manager.getCDPSession();
1768
- await cdp.send("Input.dispatchKeyEvent", {
1769
- type: event.type,
1770
- key: event.key,
1771
- code: event.code,
1772
- text: event.text,
1773
- modifiers: event.modifiers ?? 0,
1774
- windowsVirtualKeyCode: event.windowsVirtualKeyCode
1775
- });
1776
- }
999
+ if (!info || typeof info.scrollHeight !== "number") return {
1000
+ scrollY: 0,
1001
+ scrollHeight: 0,
1002
+ viewportHeight: 0,
1003
+ atTop: true,
1004
+ atBottom: true,
1005
+ percentDown: 0
1006
+ };
1007
+ const maxScroll = info.scrollHeight - info.viewportHeight;
1008
+ return {
1009
+ ...info,
1010
+ atTop: info.scrollY < 50,
1011
+ atBottom: info.scrollY >= maxScroll - 50,
1012
+ percentDown: maxScroll > 0 ? Math.round(info.scrollY / maxScroll * 100) : 0
1013
+ };
1014
+ }
1015
+ /**
1016
+ * Get the current page URL without launching the browser.
1017
+ * @param threadId - Optional thread ID for thread-isolated browsers
1018
+ * @returns The current URL string, or null if browser is not running
1019
+ */
1020
+ async getCurrentUrl(threadId) {
1021
+ if (!this.isBrowserRunning()) return null;
1022
+ try {
1023
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1024
+ if (this.threadManager.getScope() === "thread" && effectiveThreadId) {
1025
+ const manager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1026
+ if (!manager) return null;
1027
+ const url = manager.getPage().url();
1028
+ if (url && url !== "about:blank") {
1029
+ const state = this.getBrowserStateForManager(manager);
1030
+ if (state) this.threadManager.updateBrowserState(effectiveThreadId, state);
1031
+ }
1032
+ return url;
1033
+ }
1034
+ const manager = await this.getManagerForThread(threadId);
1035
+ const url = manager.getPage().url();
1036
+ if (url && url !== "about:blank") {
1037
+ const state = this.getBrowserStateForManager(manager);
1038
+ if (state) this.lastBrowserState = state;
1039
+ }
1040
+ return url;
1041
+ } catch {
1042
+ return null;
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Navigate to a URL (simple form). Used internally for restoring state on relaunch.
1047
+ */
1048
+ async navigateTo(url) {
1049
+ if (!this.isBrowserRunning()) return;
1050
+ try {
1051
+ await (await this.getPage()).goto(url, {
1052
+ timeout: this.defaultTimeout,
1053
+ waitUntil: "domcontentloaded"
1054
+ });
1055
+ } catch {}
1056
+ }
1057
+ /**
1058
+ * Get the current browser state (all tabs and active tab index).
1059
+ */
1060
+ async getBrowserState(threadId) {
1061
+ if (!this.isBrowserRunning(threadId)) return null;
1062
+ try {
1063
+ const manager = await this.getManagerForThread(threadId);
1064
+ return this.getBrowserStateForManager(manager, threadId);
1065
+ } catch {
1066
+ return null;
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Get browser state for a thread (implements abstract method from base class).
1071
+ * Sync version that uses existing manager lookup without creating sessions.
1072
+ */
1073
+ getBrowserStateForThread(threadId) {
1074
+ const effectiveThreadId = threadId ?? this.getCurrentThread() ?? _mastra_core_browser.DEFAULT_THREAD_ID;
1075
+ const manager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1076
+ if (!manager) return null;
1077
+ return this.getBrowserStateForManager(manager, effectiveThreadId);
1078
+ }
1079
+ /**
1080
+ * Get browser state from a specific manager instance.
1081
+ */
1082
+ getBrowserStateForManager(manager, threadId) {
1083
+ try {
1084
+ const stateKey = this.browserStateKey(threadId);
1085
+ const pages = manager.getPages();
1086
+ const activeIndex = manager.getActiveIndex();
1087
+ const tabs = pages.map((page) => ({ url: page.url() }));
1088
+ const activeUrl = tabs[activeIndex]?.url;
1089
+ const previousState = this.threadManager.getSavedBrowserState(stateKey) ?? this.lastBrowserState;
1090
+ const previousUrl = previousState?.tabs[previousState.activeTabIndex]?.url;
1091
+ const activeUrlChangeSource = this.getActiveUrlChangeSource(activeUrl, stateKey) ?? (previousUrl && activeUrl !== previousUrl ? "user" : void 0);
1092
+ const state = {
1093
+ tabs,
1094
+ activeTabIndex: activeIndex,
1095
+ ...this.getCloseReason(stateKey) ? { closeReason: this.getCloseReason(stateKey) } : {},
1096
+ ...activeUrlChangeSource ? { activeUrlChangeSource } : {}
1097
+ };
1098
+ this.threadManager.updateBrowserState(stateKey, state);
1099
+ this.lastBrowserState = state;
1100
+ return state;
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Get all open tabs with their URLs and titles.
1107
+ */
1108
+ async getTabState(threadId) {
1109
+ return (await this.getBrowserState(threadId))?.tabs ?? [];
1110
+ }
1111
+ /**
1112
+ * Get the active tab index.
1113
+ */
1114
+ async getActiveTabIndex(threadId) {
1115
+ if (!this.isBrowserRunning()) return 0;
1116
+ try {
1117
+ return (await this.getManagerForThread(threadId)).getActiveIndex();
1118
+ } catch {
1119
+ return 0;
1120
+ }
1121
+ }
1122
+ /**
1123
+ * Export the current browser session's storage state (cookies, localStorage) to a JSON file.
1124
+ * This can later be loaded via the `storageState` config option to restore the session.
1125
+ *
1126
+ * @param path - File path to save the storage state JSON
1127
+ * @param threadId - Optional thread ID (defaults to current thread)
1128
+ */
1129
+ async exportStorageState(path, threadId) {
1130
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1131
+ const manager = this.threadManager.getExistingManagerForThread(effectiveThreadId);
1132
+ if (!manager) throw new Error("No browser is running. Launch a browser first before exporting storage state.");
1133
+ const context = manager.getContext();
1134
+ if (!context) throw new Error("Browser context not available");
1135
+ await context.storageState({ path });
1136
+ }
1137
+ async goto(input, threadId) {
1138
+ try {
1139
+ const page = await this.getPage(threadId);
1140
+ await page.goto(input.url, {
1141
+ timeout: input.timeout ?? this.defaultTimeout,
1142
+ waitUntil: input.waitUntil ?? "domcontentloaded"
1143
+ });
1144
+ const url = page.url();
1145
+ this.markActiveUrlChangeSource("agent", url, threadId);
1146
+ return {
1147
+ success: true,
1148
+ url,
1149
+ title: await page.title(),
1150
+ hint: "Take a snapshot to see interactive elements and get refs."
1151
+ };
1152
+ } catch (error) {
1153
+ return this.createErrorFromException(error, "Goto");
1154
+ }
1155
+ }
1156
+ async snapshot(input, threadId) {
1157
+ try {
1158
+ const manager = await this.getManagerForThread(threadId);
1159
+ const page = await this.getPage(threadId);
1160
+ const snapshot = ((await manager.getSnapshot({
1161
+ interactive: input.interactiveOnly ?? true,
1162
+ compact: true
1163
+ })).tree ?? "").replace(/\[ref=(\w+)\]/g, "@$1");
1164
+ const scrollInfo = await this.getScrollInfo(threadId);
1165
+ let scrollText;
1166
+ if (scrollInfo.atTop && !scrollInfo.atBottom) scrollText = "TOP - more content below";
1167
+ else if (scrollInfo.atBottom) scrollText = "BOTTOM of page";
1168
+ else scrollText = `${scrollInfo.percentDown}% down`;
1169
+ const refs = snapshot.match(/@e\d+/g) || [];
1170
+ const elementCount = new Set(refs).size;
1171
+ return {
1172
+ success: true,
1173
+ snapshot,
1174
+ url: page.url(),
1175
+ title: await page.title(),
1176
+ elementCount,
1177
+ scroll: scrollText,
1178
+ hint: elementCount === 0 ? "No interactive elements found. Try scrolling or setting interactiveOnly:false." : void 0
1179
+ };
1180
+ } catch (error) {
1181
+ return this.createErrorFromException(error, "Snapshot");
1182
+ }
1183
+ }
1184
+ async screenshot(input, threadId) {
1185
+ try {
1186
+ const page = await this.getPage(threadId);
1187
+ const buffer = await page.screenshot({
1188
+ fullPage: input.fullPage ?? false,
1189
+ type: "png"
1190
+ });
1191
+ return {
1192
+ base64: Buffer.from(buffer).toString("base64"),
1193
+ url: page.url(),
1194
+ title: await page.title()
1195
+ };
1196
+ } catch (error) {
1197
+ return this.createErrorFromException(error, "Screenshot");
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Start a `waitForNavigation` wait (when `waitUntil` is set) and immediately
1202
+ * attach a noop catch handler so a navigation timeout/rejection can't become
1203
+ * an unhandled rejection (crashing the process) while the caller's action is
1204
+ * still pending. Callers should still `await` the returned promise to observe
1205
+ * the original error.
1206
+ */
1207
+ startNavigationWait(page, waitUntil, timeout) {
1208
+ const navigation = waitUntil ? page.waitForNavigation({
1209
+ waitUntil,
1210
+ timeout
1211
+ }) : void 0;
1212
+ navigation?.catch(() => {});
1213
+ return navigation;
1214
+ }
1215
+ async click(input, threadId) {
1216
+ try {
1217
+ const page = await this.getPage(threadId);
1218
+ const locator = await this.requireLocator(input.ref, threadId);
1219
+ if (!locator) return this.createError("stale_ref", `Ref ${input.ref} not found. The page has changed.`, "Take a new snapshot to see the current page state and get fresh refs.");
1220
+ const timeout = input.timeout ?? this.defaultTimeout;
1221
+ const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1222
+ await locator.click({
1223
+ button: input.button ?? "left",
1224
+ clickCount: input.clickCount ?? 1,
1225
+ modifiers: input.modifiers,
1226
+ timeout
1227
+ });
1228
+ await navigation;
1229
+ return {
1230
+ success: true,
1231
+ url: page.url(),
1232
+ hint: "Take a new snapshot to see updated page state and get fresh refs."
1233
+ };
1234
+ } catch (error) {
1235
+ if ((error instanceof Error ? error.message : String(error)).includes("intercepts pointer events")) return this.createError("element_blocked", `Element ${input.ref} is blocked by another element.`, "Take a new snapshot to see what is blocking. Dismiss any modals or scroll the element into view.");
1236
+ return this.createErrorFromException(error, "Click");
1237
+ }
1238
+ }
1239
+ async type(input, threadId) {
1240
+ try {
1241
+ const page = await this.getPage(threadId);
1242
+ const locator = await this.requireLocator(input.ref, threadId);
1243
+ if (!locator) return this.createError("stale_ref", `Ref ${input.ref} not found. The page has changed.`, "Take a new snapshot to see the current page state and get fresh refs.");
1244
+ if (input.clear) await locator.fill("", { timeout: this.defaultTimeout });
1245
+ if (input.delay) {
1246
+ await locator.focus();
1247
+ for (const char of input.text) {
1248
+ await page.keyboard.press(char);
1249
+ await new Promise((r) => setTimeout(r, input.delay));
1250
+ }
1251
+ } else await locator.fill(input.text, { timeout: this.defaultTimeout });
1252
+ return {
1253
+ success: true,
1254
+ value: await locator.inputValue({ timeout: 1e3 }).catch(() => input.text),
1255
+ url: page.url(),
1256
+ hint: "Take a new snapshot if you need to interact with more elements."
1257
+ };
1258
+ } catch (error) {
1259
+ const errorMsg = error instanceof Error ? error.message : String(error);
1260
+ if (errorMsg.includes("is not an <input>") || errorMsg.includes("not an input") || errorMsg.includes("Cannot type") || errorMsg.includes("not focusable")) return this.createError("not_focusable", `Element ${input.ref} is not a text input field.`, "Take a new snapshot and look for elements with role \"textbox\" or \"searchbox\".");
1261
+ return this.createErrorFromException(error, "Type");
1262
+ }
1263
+ }
1264
+ async press(input, threadId) {
1265
+ try {
1266
+ const page = await this.getPage(threadId);
1267
+ const timeout = input.timeout ?? this.defaultTimeout;
1268
+ const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1269
+ await page.keyboard.press(input.key);
1270
+ await navigation;
1271
+ return {
1272
+ success: true,
1273
+ url: page.url(),
1274
+ hint: "Take a new snapshot if the page may have changed."
1275
+ };
1276
+ } catch (error) {
1277
+ return this.createErrorFromException(error, "Press");
1278
+ }
1279
+ }
1280
+ async select(input, threadId) {
1281
+ try {
1282
+ const page = await this.getPage(threadId);
1283
+ const locator = await this.requireLocator(input.ref, threadId);
1284
+ if (!locator) return this.createError("stale_ref", `Ref ${input.ref} not found. The page has changed.`, "Take a new snapshot to get fresh refs.");
1285
+ const selectValue = {};
1286
+ if (input.value) selectValue.value = input.value;
1287
+ if (input.label) selectValue.label = input.label;
1288
+ if (input.index !== void 0) selectValue.index = input.index;
1289
+ const timeout = input.timeout ?? this.defaultTimeout;
1290
+ const navigation = this.startNavigationWait(page, input.waitUntil, timeout);
1291
+ const selected = await locator.selectOption(selectValue, { timeout });
1292
+ await navigation;
1293
+ return {
1294
+ success: true,
1295
+ selected,
1296
+ url: page.url(),
1297
+ hint: "Selection complete. Take a snapshot if you need to continue."
1298
+ };
1299
+ } catch (error) {
1300
+ return this.createErrorFromException(error, "Select");
1301
+ }
1302
+ }
1303
+ async scroll(input, threadId) {
1304
+ try {
1305
+ const page = await this.getPage(threadId);
1306
+ if (input.ref) {
1307
+ const locator = await this.requireLocator(input.ref, threadId);
1308
+ if (locator) await locator.scrollIntoViewIfNeeded({ timeout: this.defaultTimeout });
1309
+ } else {
1310
+ const direction = input.direction;
1311
+ const amount = input.amount ?? 300;
1312
+ let deltaX = 0;
1313
+ let deltaY = 0;
1314
+ switch (direction) {
1315
+ case "up":
1316
+ deltaY = -amount;
1317
+ break;
1318
+ case "down":
1319
+ deltaY = amount;
1320
+ break;
1321
+ case "left":
1322
+ deltaX = -amount;
1323
+ break;
1324
+ case "right":
1325
+ deltaX = amount;
1326
+ break;
1327
+ }
1328
+ await page.evaluate(({ x, y }) => {
1329
+ globalThis.scrollBy(x, y);
1330
+ }, {
1331
+ x: deltaX,
1332
+ y: deltaY
1333
+ });
1334
+ }
1335
+ const scrollInfo = await this.getScrollInfo(threadId);
1336
+ let scrollText;
1337
+ if (scrollInfo.atTop && !scrollInfo.atBottom) scrollText = "TOP - more content below";
1338
+ else if (scrollInfo.atBottom) scrollText = "BOTTOM of page";
1339
+ else scrollText = `${scrollInfo.percentDown}% down`;
1340
+ return {
1341
+ success: true,
1342
+ position: {
1343
+ x: 0,
1344
+ y: scrollInfo.scrollY
1345
+ },
1346
+ scroll: scrollText,
1347
+ hint: "Take a new snapshot to see elements in the new viewport."
1348
+ };
1349
+ } catch (error) {
1350
+ return this.createErrorFromException(error, "Scroll");
1351
+ }
1352
+ }
1353
+ async hover(input, threadId) {
1354
+ try {
1355
+ const page = await this.getPage(threadId);
1356
+ const locator = await this.requireLocator(input.ref, threadId);
1357
+ if (!locator) return this.createError("stale_ref", `Ref ${input.ref} not found. The page has changed.`, "Take a new snapshot to get fresh refs.");
1358
+ await locator.hover({ timeout: this.defaultTimeout });
1359
+ return {
1360
+ success: true,
1361
+ url: page.url(),
1362
+ hint: "Take a new snapshot to see any hover-triggered elements (dropdowns, tooltips)."
1363
+ };
1364
+ } catch (error) {
1365
+ return this.createErrorFromException(error, "Hover");
1366
+ }
1367
+ }
1368
+ async back(threadId) {
1369
+ try {
1370
+ const page = await this.getPage(threadId);
1371
+ await page.goBack({ timeout: this.defaultTimeout });
1372
+ const url = page.url();
1373
+ this.markActiveUrlChangeSource("agent", url, threadId);
1374
+ return {
1375
+ success: true,
1376
+ url,
1377
+ title: await page.title(),
1378
+ hint: "Take a new snapshot to see the previous page."
1379
+ };
1380
+ } catch (error) {
1381
+ return this.createErrorFromException(error, "Back");
1382
+ }
1383
+ }
1384
+ async dialog(input, threadId) {
1385
+ try {
1386
+ const page = await this.getPage(threadId);
1387
+ const locator = await this.requireLocator(input.triggerRef, threadId);
1388
+ if (!locator) return this.createError("stale_ref", `Trigger ref ${input.triggerRef} not found.`, "Take a new snapshot to get fresh refs.");
1389
+ return new Promise((resolve, reject) => {
1390
+ const timeout = setTimeout(() => {
1391
+ page.off("dialog", dialogHandler);
1392
+ reject(/* @__PURE__ */ new Error(`No dialog appeared after clicking ${input.triggerRef}. The element may not trigger a dialog.`));
1393
+ }, this.defaultTimeout);
1394
+ const dialogHandler = async (dialog) => {
1395
+ clearTimeout(timeout);
1396
+ try {
1397
+ const dialogType = dialog.type();
1398
+ const message = dialog.message();
1399
+ if (input.action === "accept") await dialog.accept(input.text);
1400
+ else await dialog.dismiss();
1401
+ resolve({
1402
+ success: true,
1403
+ action: input.action,
1404
+ dialogType,
1405
+ message,
1406
+ hint: "Dialog handled. Take a snapshot to continue."
1407
+ });
1408
+ } catch (e) {
1409
+ reject(e);
1410
+ }
1411
+ };
1412
+ page.once("dialog", dialogHandler);
1413
+ locator.click({ timeout: this.defaultTimeout }).catch((e) => {
1414
+ clearTimeout(timeout);
1415
+ page.off("dialog", dialogHandler);
1416
+ reject(e);
1417
+ });
1418
+ });
1419
+ } catch (error) {
1420
+ return this.createErrorFromException(error, "Dialog");
1421
+ }
1422
+ }
1423
+ async wait(input, threadId) {
1424
+ try {
1425
+ const timeout = input.timeout ?? this.defaultTimeout;
1426
+ if (input.ref) {
1427
+ const locator = await this.requireLocator(input.ref, threadId);
1428
+ if (!locator) return this.createError("stale_ref", `Ref ${input.ref} not found.`, "Take a new snapshot to get fresh refs.");
1429
+ const state = input.state ?? "visible";
1430
+ await locator.waitFor({
1431
+ state,
1432
+ timeout
1433
+ });
1434
+ return {
1435
+ success: true,
1436
+ hint: `Element is now ${state}. Take a snapshot to continue.`
1437
+ };
1438
+ } else {
1439
+ await (await this.getPage(threadId)).waitForTimeout(timeout);
1440
+ return {
1441
+ success: true,
1442
+ hint: "Wait complete. Take a snapshot to see current state."
1443
+ };
1444
+ }
1445
+ } catch (error) {
1446
+ return this.createErrorFromException(error, "Wait");
1447
+ }
1448
+ }
1449
+ async tabs(input, threadId) {
1450
+ try {
1451
+ const browser = await this.getManagerForThread(threadId);
1452
+ if (!browser) return this.createError("browser_closed", "Browser not launched", "Call a navigation tool first to launch the browser.");
1453
+ switch (input.action) {
1454
+ case "list":
1455
+ if (!browser.listTabs) return this.createError("browser_error", "Tab management not supported", "This browser provider does not support tab management.");
1456
+ return {
1457
+ success: true,
1458
+ tabs: await browser.listTabs(),
1459
+ hint: "Use browser_tabs with action:\"switch\" and index to change tabs."
1460
+ };
1461
+ case "new": {
1462
+ if (!browser.newTab) return this.createError("browser_error", "Tab management not supported", "This browser provider does not support tab management.");
1463
+ const result = await browser.newTab();
1464
+ if (input.url) {
1465
+ const page = await this.getPage(threadId);
1466
+ await page.goto(input.url);
1467
+ this.markActiveUrlChangeSource("agent", page.url(), threadId);
1468
+ }
1469
+ this.updateSessionBrowserState(threadId);
1470
+ return {
1471
+ success: true,
1472
+ ...result,
1473
+ hint: "New tab opened. Take a snapshot to see its content."
1474
+ };
1475
+ }
1476
+ case "switch": {
1477
+ if (!browser.switchTo) return this.createError("browser_error", "Tab management not supported", "This browser provider does not support tab management.");
1478
+ await browser.switchTo(input.index);
1479
+ await this.reconnectScreencastForThread(threadId, "tab switch");
1480
+ const page = browser.getPage();
1481
+ const pageUrl = page.url();
1482
+ this.markActiveUrlChangeSource("agent", pageUrl, threadId);
1483
+ const streamKey = this.getStreamKey(threadId);
1484
+ const stream = this.activeScreencastStreams.get(streamKey);
1485
+ if (pageUrl && stream?.isActive()) stream.emitUrl(pageUrl);
1486
+ this.updateSessionBrowserState(threadId);
1487
+ return {
1488
+ success: true,
1489
+ index: input.index,
1490
+ url: pageUrl,
1491
+ title: await page.title(),
1492
+ hint: "Tab switched. Take a snapshot to see its content."
1493
+ };
1494
+ }
1495
+ case "close": {
1496
+ if (!browser.closeTab) return this.createError("browser_error", "Tab management not supported", "This browser provider does not support tab management.");
1497
+ await browser.closeTab(input.index);
1498
+ await this.reconnectScreencastForThread(threadId, "tab close");
1499
+ this.updateSessionBrowserState(threadId);
1500
+ const tabsList = await browser.listTabs?.() ?? [];
1501
+ return {
1502
+ success: true,
1503
+ remaining: tabsList.length,
1504
+ hint: tabsList.length > 0 ? "Tab closed. Take a snapshot to see current tab." : "All tabs closed."
1505
+ };
1506
+ }
1507
+ default: return this.createError("browser_error", `Unknown tabs action: ${input.action}`, "Use \"list\", \"new\", \"switch\", or \"close\".");
1508
+ }
1509
+ } catch (error) {
1510
+ return this.createErrorFromException(error, "Tabs");
1511
+ }
1512
+ }
1513
+ async drag(input, threadId) {
1514
+ try {
1515
+ const page = await this.getPage(threadId);
1516
+ let sourceLocator = null;
1517
+ if (input.sourceRef) sourceLocator = await this.requireLocator(input.sourceRef, threadId);
1518
+ else if (input.sourceSelector) sourceLocator = page.locator(input.sourceSelector);
1519
+ if (!sourceLocator) return this.createError("stale_ref", input.sourceRef ? `Source ref ${input.sourceRef} not found.` : "No source element specified. Provide sourceRef or sourceSelector.", input.sourceRef ? "Take a new snapshot to get fresh refs, or use sourceSelector for elements not in the accessibility tree." : void 0);
1520
+ let targetLocator = null;
1521
+ if (input.targetRef) targetLocator = await this.requireLocator(input.targetRef, threadId);
1522
+ else if (input.targetSelector) targetLocator = page.locator(input.targetSelector);
1523
+ if (!targetLocator) return this.createError("stale_ref", input.targetRef ? `Target ref ${input.targetRef} not found.` : "No target element specified. Provide targetRef or targetSelector.", input.targetRef ? "Take a new snapshot to get fresh refs, or use targetSelector for elements not in the accessibility tree." : void 0);
1524
+ await sourceLocator.dragTo(targetLocator, { timeout: this.defaultTimeout });
1525
+ return {
1526
+ success: true,
1527
+ url: page.url(),
1528
+ hint: "Drag complete. Take a snapshot to see the result."
1529
+ };
1530
+ } catch (error) {
1531
+ return this.createErrorFromException(error, "Drag");
1532
+ }
1533
+ }
1534
+ async evaluate(input, threadId) {
1535
+ try {
1536
+ return {
1537
+ success: true,
1538
+ result: await (await this.getPage(threadId)).evaluate(input.script),
1539
+ hint: "JavaScript executed. Take a snapshot if the page may have changed."
1540
+ };
1541
+ } catch (error) {
1542
+ return this.createErrorFromException(error, "Evaluate");
1543
+ }
1544
+ }
1545
+ async closeBrowser() {
1546
+ try {
1547
+ await this.close();
1548
+ return {
1549
+ success: true,
1550
+ hint: "Browser closed. Call browser_goto to start a new session."
1551
+ };
1552
+ } catch (error) {
1553
+ return this.createErrorFromException(error, "Close");
1554
+ }
1555
+ }
1556
+ async startScreencast(_options) {
1557
+ const requestedThreadId = _options?.threadId;
1558
+ const effectiveThreadId = this.getScope() === "thread" ? requestedThreadId ?? this.getCurrentThread() ?? _mastra_core_browser.DEFAULT_THREAD_ID : requestedThreadId;
1559
+ let browserManager;
1560
+ if (this.getScope() === "thread") browserManager = await this.getManagerForThread(effectiveThreadId);
1561
+ else {
1562
+ if (!this.sharedManager) throw new Error("Browser not launched");
1563
+ browserManager = this.sharedManager;
1564
+ }
1565
+ const stream = new _mastra_core_browser.ScreencastStreamImpl({
1566
+ getCdpSession: async () => {
1567
+ const currentPage = browserManager.getPage();
1568
+ if (!currentPage) throw new Error("No active page available");
1569
+ return await currentPage.context().newCDPSession(currentPage);
1570
+ },
1571
+ isBrowserRunning: () => browserManager.isLaunched()
1572
+ }, _options);
1573
+ const streamKey = this.getStreamKey(effectiveThreadId);
1574
+ this.activeScreencastStreams.set(streamKey, stream);
1575
+ const context = browserManager.getContext();
1576
+ if (context) {
1577
+ const onNewPage = (_newPage) => {
1578
+ setTimeout(() => {
1579
+ if (stream.isActive()) stream.reconnect().catch(() => {});
1580
+ }, 100);
1581
+ };
1582
+ context.on("page", onNewPage);
1583
+ const pageCloseHandlers = /* @__PURE__ */ new Map();
1584
+ const frameNavigatedHandlers = /* @__PURE__ */ new Map();
1585
+ const setupPageListeners = (page) => {
1586
+ const onFrameNavigated = (frame) => {
1587
+ if (!frame.parentFrame()) {
1588
+ stream.emitUrl(frame.url());
1589
+ this.updateSessionBrowserState(effectiveThreadId);
1590
+ }
1591
+ };
1592
+ page.on("framenavigated", onFrameNavigated);
1593
+ frameNavigatedHandlers.set(page, onFrameNavigated);
1594
+ const onClose = () => {
1595
+ pageCloseHandlers.delete(page);
1596
+ const navHandler = frameNavigatedHandlers.get(page);
1597
+ if (navHandler) {
1598
+ page.off("framenavigated", navHandler);
1599
+ frameNavigatedHandlers.delete(page);
1600
+ }
1601
+ setTimeout(() => {
1602
+ const remainingPages = browserManager.getPages();
1603
+ if (stream.isActive() && remainingPages.length > 0) {
1604
+ stream.reconnect().catch(() => {});
1605
+ const activePage = remainingPages[browserManager.getActiveIndex()] || remainingPages[0];
1606
+ if (activePage) {
1607
+ const url = activePage.url();
1608
+ if (url && url !== "about:blank") stream.emitUrl(url);
1609
+ }
1610
+ }
1611
+ }, 100);
1612
+ };
1613
+ page.once("close", onClose);
1614
+ pageCloseHandlers.set(page, onClose);
1615
+ };
1616
+ const setupPageCloseListener = setupPageListeners;
1617
+ for (const page of browserManager.getPages()) setupPageCloseListener(page);
1618
+ const onNewPageWithCloseListener = (newPage) => {
1619
+ setupPageCloseListener(newPage);
1620
+ const url = newPage.url();
1621
+ if (url && url !== "about:blank") stream.emitUrl(url);
1622
+ onNewPage(newPage);
1623
+ };
1624
+ context.off("page", onNewPage);
1625
+ context.on("page", onNewPageWithCloseListener);
1626
+ stream.once("stop", () => {
1627
+ context.off("page", onNewPageWithCloseListener);
1628
+ for (const [page, handler] of pageCloseHandlers) page.off("close", handler);
1629
+ pageCloseHandlers.clear();
1630
+ for (const [page, handler] of frameNavigatedHandlers) page.off("framenavigated", handler);
1631
+ frameNavigatedHandlers.clear();
1632
+ this.activeScreencastStreams.delete(streamKey);
1633
+ });
1634
+ }
1635
+ await stream.start();
1636
+ return stream;
1637
+ }
1638
+ async injectMouseEvent(event, threadId) {
1639
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1640
+ await (await this.getManagerForThread(effectiveThreadId)).injectMouseEvent(event);
1641
+ }
1642
+ async injectKeyboardEvent(event, threadId) {
1643
+ const effectiveThreadId = threadId ?? this.getCurrentThread();
1644
+ await (await (await this.getManagerForThread(effectiveThreadId)).getCDPSession()).send("Input.dispatchKeyEvent", {
1645
+ type: event.type,
1646
+ key: event.key,
1647
+ code: event.code,
1648
+ text: event.text,
1649
+ modifiers: event.modifiers ?? 0,
1650
+ windowsVirtualKeyCode: event.windowsVirtualKeyCode
1651
+ });
1652
+ }
1777
1653
  };
1778
-
1654
+ //#endregion
1779
1655
  exports.AgentBrowser = AgentBrowser;
1780
1656
  exports.AgentBrowserThreadManager = AgentBrowserThreadManager;
1781
1657
  exports.BROWSER_TOOLS = BROWSER_TOOLS;
@@ -1797,5 +1673,5 @@ exports.snapshotInputSchema = snapshotInputSchema;
1797
1673
  exports.tabsInputSchema = tabsInputSchema;
1798
1674
  exports.typeInputSchema = typeInputSchema;
1799
1675
  exports.waitInputSchema = waitInputSchema;
1800
- //# sourceMappingURL=index.cjs.map
1676
+
1801
1677
  //# sourceMappingURL=index.cjs.map