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