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