@browserbasehq/convex-stagehand 0.0.3 → 0.1.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/README.md +18 -19
- package/dist/client/index.d.ts +61 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +89 -18
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +181 -10
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/component.js +1 -1
- package/dist/component/_generated/dataModel.d.ts +33 -2
- package/dist/component/_generated/dataModel.d.ts.map +1 -1
- package/dist/component/api.d.ts +78 -17
- package/dist/component/api.d.ts.map +1 -1
- package/dist/component/api.js +93 -33
- package/dist/component/api.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -1
- package/dist/component/lib.d.ts +23 -5
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +489 -50
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +3 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +1 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/package.json +6 -9
- package/src/client/index.ts +151 -21
- package/src/component/_generated/component.ts +213 -12
- package/src/component/_generated/dataModel.ts +45 -2
- package/src/component/api.ts +225 -68
- package/src/component/lib.ts +639 -57
- package/src/component/schema.ts +8 -0
package/dist/component/lib.js
CHANGED
|
@@ -4,22 +4,199 @@
|
|
|
4
4
|
* AI-powered browser automation actions using the Stagehand REST API.
|
|
5
5
|
* Supports both automatic session management and manual session control.
|
|
6
6
|
*/
|
|
7
|
-
import { action } from "./_generated/server.js";
|
|
7
|
+
import { action, internalMutation, internalQuery } from "./_generated/server.js";
|
|
8
|
+
import { internal } from "./_generated/api.js";
|
|
8
9
|
import { v } from "convex/values";
|
|
9
10
|
import * as api from "./api.js";
|
|
11
|
+
const DEFAULT_BROWSERBASE_REGION = "us-west-2";
|
|
12
|
+
const browserbaseRegionValidator = v.union(v.literal("us-west-2"), v.literal("us-east-1"), v.literal("eu-central-1"), v.literal("ap-southeast-1"));
|
|
13
|
+
const sessionStatusValidator = v.union(v.literal("active"), v.literal("completed"), v.literal("error"));
|
|
14
|
+
const sessionOperationValidator = v.union(v.literal("extract"), v.literal("act"), v.literal("observe"), v.literal("workflow"));
|
|
10
15
|
const observedActionValidator = v.object({
|
|
11
16
|
description: v.string(),
|
|
12
17
|
selector: v.string(),
|
|
13
|
-
method: v.string(),
|
|
18
|
+
method: v.optional(v.string()),
|
|
14
19
|
arguments: v.optional(v.array(v.string())),
|
|
20
|
+
backendNodeId: v.optional(v.number()),
|
|
15
21
|
});
|
|
16
22
|
const waitUntilValidator = v.union(v.literal("load"), v.literal("domcontentloaded"), v.literal("networkidle"));
|
|
23
|
+
/** Session-level config forwarded from the client's StagehandConfig for ephemeral sessions. */
|
|
24
|
+
const sessionConfigValidator = v.optional(v.object({
|
|
25
|
+
domSettleTimeoutMs: v.optional(v.number()),
|
|
26
|
+
selfHeal: v.optional(v.boolean()),
|
|
27
|
+
systemPrompt: v.optional(v.string()),
|
|
28
|
+
verbose: v.optional(v.number()),
|
|
29
|
+
experimental: v.optional(v.boolean()),
|
|
30
|
+
}));
|
|
31
|
+
const modelValidator = v.optional(v.union(v.string(), v.object({
|
|
32
|
+
modelName: v.optional(v.string()),
|
|
33
|
+
apiKey: v.optional(v.string()),
|
|
34
|
+
baseURL: v.optional(v.string()),
|
|
35
|
+
provider: v.optional(v.string()),
|
|
36
|
+
})));
|
|
37
|
+
const variablesValidator = v.optional(v.record(v.string(), v.string()));
|
|
17
38
|
const agentActionValidator = v.object({
|
|
18
39
|
type: v.string(),
|
|
19
40
|
action: v.optional(v.string()),
|
|
20
41
|
reasoning: v.optional(v.string()),
|
|
21
42
|
timeMs: v.optional(v.number()),
|
|
43
|
+
taskCompleted: v.optional(v.boolean()),
|
|
44
|
+
pageText: v.optional(v.string()),
|
|
45
|
+
pageUrl: v.optional(v.string()),
|
|
46
|
+
instruction: v.optional(v.string()),
|
|
22
47
|
});
|
|
48
|
+
function isBrowserbaseRegion(value) {
|
|
49
|
+
return (value === "us-west-2" ||
|
|
50
|
+
value === "us-east-1" ||
|
|
51
|
+
value === "eu-central-1" ||
|
|
52
|
+
value === "ap-southeast-1");
|
|
53
|
+
}
|
|
54
|
+
function getRequestedRegion(browserbaseSessionCreateParams) {
|
|
55
|
+
const maybeRegion = browserbaseSessionCreateParams?.region;
|
|
56
|
+
if (isBrowserbaseRegion(maybeRegion)) {
|
|
57
|
+
return maybeRegion;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function extractRegionFromError(error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
const match = message.match(/Session is in region '([^']+)'/i);
|
|
64
|
+
const parsedRegion = match?.[1];
|
|
65
|
+
if (isBrowserbaseRegion(parsedRegion)) {
|
|
66
|
+
return parsedRegion;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
export const upsertSessionMetadata = internalMutation({
|
|
71
|
+
args: {
|
|
72
|
+
sessionId: v.string(),
|
|
73
|
+
region: v.optional(browserbaseRegionValidator),
|
|
74
|
+
status: v.optional(sessionStatusValidator),
|
|
75
|
+
operation: v.optional(sessionOperationValidator),
|
|
76
|
+
url: v.optional(v.string()),
|
|
77
|
+
endedAt: v.optional(v.number()),
|
|
78
|
+
error: v.optional(v.string()),
|
|
79
|
+
},
|
|
80
|
+
returns: v.null(),
|
|
81
|
+
handler: async (ctx, args) => {
|
|
82
|
+
const existing = await ctx.db
|
|
83
|
+
.query("sessions")
|
|
84
|
+
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
|
|
85
|
+
.first();
|
|
86
|
+
if (existing) {
|
|
87
|
+
const patch = {};
|
|
88
|
+
if (args.region !== undefined)
|
|
89
|
+
patch.region = args.region;
|
|
90
|
+
if (args.status !== undefined)
|
|
91
|
+
patch.status = args.status;
|
|
92
|
+
if (args.operation !== undefined)
|
|
93
|
+
patch.operation = args.operation;
|
|
94
|
+
if (args.url !== undefined)
|
|
95
|
+
patch.url = args.url;
|
|
96
|
+
if (args.endedAt !== undefined)
|
|
97
|
+
patch.endedAt = args.endedAt;
|
|
98
|
+
if (args.error !== undefined)
|
|
99
|
+
patch.error = args.error;
|
|
100
|
+
await ctx.db.patch(existing._id, patch);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
await ctx.db.insert("sessions", {
|
|
104
|
+
sessionId: args.sessionId,
|
|
105
|
+
region: args.region,
|
|
106
|
+
startedAt: Date.now(),
|
|
107
|
+
endedAt: args.endedAt,
|
|
108
|
+
status: args.status ?? "active",
|
|
109
|
+
operation: args.operation ?? "workflow",
|
|
110
|
+
url: args.url ?? "",
|
|
111
|
+
error: args.error,
|
|
112
|
+
});
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
export const getSessionRegion = internalQuery({
|
|
117
|
+
args: {
|
|
118
|
+
sessionId: v.string(),
|
|
119
|
+
},
|
|
120
|
+
returns: v.union(browserbaseRegionValidator, v.null()),
|
|
121
|
+
handler: async (ctx, args) => {
|
|
122
|
+
const session = await ctx.db
|
|
123
|
+
.query("sessions")
|
|
124
|
+
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
|
|
125
|
+
.first();
|
|
126
|
+
return session?.region ?? null;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
async function resolveSessionRegion(ctx, sessionId, fallback) {
|
|
130
|
+
const storedRegion = await ctx.runQuery(internal.lib.getSessionRegion, {
|
|
131
|
+
sessionId,
|
|
132
|
+
});
|
|
133
|
+
return storedRegion ?? fallback ?? undefined;
|
|
134
|
+
}
|
|
135
|
+
async function persistSessionMetadata(ctx, args) {
|
|
136
|
+
await ctx.runMutation(internal.lib.upsertSessionMetadata, args);
|
|
137
|
+
}
|
|
138
|
+
async function runWithRegionRetry(ctx, args) {
|
|
139
|
+
try {
|
|
140
|
+
return await args.run(args.initialRegion);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const parsedRegion = extractRegionFromError(error);
|
|
144
|
+
if (!parsedRegion || parsedRegion === args.initialRegion) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
await persistSessionMetadata(ctx, {
|
|
148
|
+
sessionId: args.sessionId,
|
|
149
|
+
region: parsedRegion,
|
|
150
|
+
status: "active",
|
|
151
|
+
});
|
|
152
|
+
if (args.onRegionResolved) {
|
|
153
|
+
await args.onRegionResolved(parsedRegion);
|
|
154
|
+
}
|
|
155
|
+
return args.run(parsedRegion);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function endSessionWithRouting(ctx, args) {
|
|
159
|
+
try {
|
|
160
|
+
let resolvedRegion = (await resolveSessionRegion(ctx, args.sessionId, args.fallbackRegion)) ??
|
|
161
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
162
|
+
try {
|
|
163
|
+
await runWithRegionRetry(ctx, {
|
|
164
|
+
sessionId: args.sessionId,
|
|
165
|
+
initialRegion: resolvedRegion,
|
|
166
|
+
onRegionResolved: async (region) => {
|
|
167
|
+
resolvedRegion = region;
|
|
168
|
+
},
|
|
169
|
+
run: async (region) => {
|
|
170
|
+
await api.endSession(args.sessionId, args.config, region);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
await persistSessionMetadata(ctx, {
|
|
174
|
+
sessionId: args.sessionId,
|
|
175
|
+
region: resolvedRegion,
|
|
176
|
+
status: "completed",
|
|
177
|
+
endedAt: Date.now(),
|
|
178
|
+
});
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
try {
|
|
183
|
+
await persistSessionMetadata(ctx, {
|
|
184
|
+
sessionId: args.sessionId,
|
|
185
|
+
region: resolvedRegion,
|
|
186
|
+
status: "error",
|
|
187
|
+
error: "Failed to end Stagehand session",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Best-effort metadata persistence — don't mask the original failure.
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
23
200
|
/**
|
|
24
201
|
* Start a new browser session.
|
|
25
202
|
* Returns session info including cdpUrl for direct Playwright/Puppeteer connection.
|
|
@@ -31,22 +208,26 @@ export const startSession = action({
|
|
|
31
208
|
modelApiKey: v.string(),
|
|
32
209
|
modelName: v.optional(v.string()),
|
|
33
210
|
url: v.string(),
|
|
34
|
-
|
|
211
|
+
browserbaseSessionID: v.optional(v.string()),
|
|
35
212
|
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
213
|
+
model: modelValidator,
|
|
36
214
|
options: v.optional(v.object({
|
|
37
215
|
timeout: v.optional(v.number()),
|
|
38
216
|
waitUntil: v.optional(waitUntilValidator),
|
|
39
217
|
domSettleTimeoutMs: v.optional(v.number()),
|
|
40
218
|
selfHeal: v.optional(v.boolean()),
|
|
41
219
|
systemPrompt: v.optional(v.string()),
|
|
220
|
+
verbose: v.optional(v.number()),
|
|
221
|
+
experimental: v.optional(v.boolean()),
|
|
42
222
|
})),
|
|
43
223
|
},
|
|
44
224
|
returns: v.object({
|
|
45
225
|
sessionId: v.string(),
|
|
46
|
-
browserbaseSessionId: v.optional(v.string()),
|
|
47
226
|
cdpUrl: v.optional(v.string()),
|
|
48
227
|
}),
|
|
49
|
-
handler: async (
|
|
228
|
+
handler: async (ctx, args) => {
|
|
229
|
+
let resolvedRegion = getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
230
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
50
231
|
const config = {
|
|
51
232
|
browserbaseApiKey: args.browserbaseApiKey,
|
|
52
233
|
browserbaseProjectId: args.browserbaseProjectId,
|
|
@@ -54,25 +235,45 @@ export const startSession = action({
|
|
|
54
235
|
modelName: args.modelName,
|
|
55
236
|
};
|
|
56
237
|
const session = await api.startSession(config, {
|
|
57
|
-
|
|
238
|
+
browserbaseSessionID: args.browserbaseSessionID,
|
|
58
239
|
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
240
|
+
model: args.model,
|
|
59
241
|
domSettleTimeoutMs: args.options?.domSettleTimeoutMs,
|
|
60
242
|
selfHeal: args.options?.selfHeal,
|
|
61
243
|
systemPrompt: args.options?.systemPrompt,
|
|
244
|
+
verbose: args.options?.verbose,
|
|
245
|
+
experimental: args.options?.experimental,
|
|
246
|
+
});
|
|
247
|
+
await persistSessionMetadata(ctx, {
|
|
248
|
+
sessionId: session.sessionId,
|
|
249
|
+
region: resolvedRegion,
|
|
250
|
+
status: "active",
|
|
251
|
+
operation: "workflow",
|
|
252
|
+
url: args.url,
|
|
62
253
|
});
|
|
63
254
|
try {
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
255
|
+
await runWithRegionRetry(ctx, {
|
|
256
|
+
sessionId: session.sessionId,
|
|
257
|
+
initialRegion: resolvedRegion,
|
|
258
|
+
onRegionResolved: async (region) => {
|
|
259
|
+
resolvedRegion = region;
|
|
260
|
+
},
|
|
261
|
+
run: async (region) => api.navigate(session.sessionId, args.url, config, {
|
|
262
|
+
waitUntil: args.options?.waitUntil,
|
|
263
|
+
timeout: args.options?.timeout,
|
|
264
|
+
}, region),
|
|
67
265
|
});
|
|
68
266
|
return {
|
|
69
267
|
sessionId: session.sessionId,
|
|
70
|
-
|
|
71
|
-
cdpUrl: session.cdpUrl,
|
|
268
|
+
cdpUrl: session.cdpUrl ?? undefined,
|
|
72
269
|
};
|
|
73
270
|
}
|
|
74
271
|
catch (error) {
|
|
75
|
-
await
|
|
272
|
+
await endSessionWithRouting(ctx, {
|
|
273
|
+
sessionId: session.sessionId,
|
|
274
|
+
config,
|
|
275
|
+
fallbackRegion: resolvedRegion,
|
|
276
|
+
});
|
|
76
277
|
throw error;
|
|
77
278
|
}
|
|
78
279
|
},
|
|
@@ -85,17 +286,22 @@ export const endSession = action({
|
|
|
85
286
|
browserbaseApiKey: v.string(),
|
|
86
287
|
browserbaseProjectId: v.string(),
|
|
87
288
|
modelApiKey: v.string(),
|
|
289
|
+
modelName: v.optional(v.string()),
|
|
88
290
|
sessionId: v.string(),
|
|
89
291
|
},
|
|
90
292
|
returns: v.object({ success: v.boolean() }),
|
|
91
|
-
handler: async (
|
|
293
|
+
handler: async (ctx, args) => {
|
|
92
294
|
const config = {
|
|
93
295
|
browserbaseApiKey: args.browserbaseApiKey,
|
|
94
296
|
browserbaseProjectId: args.browserbaseProjectId,
|
|
95
297
|
modelApiKey: args.modelApiKey,
|
|
298
|
+
modelName: args.modelName,
|
|
96
299
|
};
|
|
97
|
-
await
|
|
98
|
-
|
|
300
|
+
const success = await endSessionWithRouting(ctx, {
|
|
301
|
+
sessionId: args.sessionId,
|
|
302
|
+
config,
|
|
303
|
+
});
|
|
304
|
+
return { success };
|
|
99
305
|
},
|
|
100
306
|
});
|
|
101
307
|
/**
|
|
@@ -114,13 +320,16 @@ export const extract = action({
|
|
|
114
320
|
instruction: v.string(),
|
|
115
321
|
schema: v.any(),
|
|
116
322
|
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
323
|
+
model: modelValidator,
|
|
324
|
+
sessionConfig: sessionConfigValidator,
|
|
117
325
|
options: v.optional(v.object({
|
|
118
326
|
timeout: v.optional(v.number()),
|
|
119
327
|
waitUntil: v.optional(waitUntilValidator),
|
|
328
|
+
selector: v.optional(v.string()),
|
|
120
329
|
})),
|
|
121
330
|
},
|
|
122
331
|
returns: v.any(),
|
|
123
|
-
handler: async (
|
|
332
|
+
handler: async (ctx, args) => {
|
|
124
333
|
if (!args.sessionId && !args.url) {
|
|
125
334
|
throw new Error("Either sessionId or url must be provided");
|
|
126
335
|
}
|
|
@@ -132,28 +341,73 @@ export const extract = action({
|
|
|
132
341
|
};
|
|
133
342
|
const ownSession = !args.sessionId;
|
|
134
343
|
let sessionId = args.sessionId;
|
|
344
|
+
let resolvedRegion;
|
|
135
345
|
if (ownSession) {
|
|
346
|
+
resolvedRegion =
|
|
347
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
348
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
136
349
|
const session = await api.startSession(config, {
|
|
137
350
|
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
351
|
+
model: args.model,
|
|
352
|
+
...args.sessionConfig,
|
|
138
353
|
});
|
|
139
354
|
sessionId = session.sessionId;
|
|
355
|
+
await persistSessionMetadata(ctx, {
|
|
356
|
+
sessionId,
|
|
357
|
+
region: resolvedRegion,
|
|
358
|
+
status: "active",
|
|
359
|
+
operation: "extract",
|
|
360
|
+
url: args.url,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (!sessionId) {
|
|
364
|
+
throw new Error("Failed to initialize session");
|
|
365
|
+
}
|
|
366
|
+
if (!ownSession) {
|
|
367
|
+
resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
|
|
140
368
|
}
|
|
141
369
|
try {
|
|
142
370
|
if (ownSession && args.url) {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
371
|
+
await runWithRegionRetry(ctx, {
|
|
372
|
+
sessionId,
|
|
373
|
+
initialRegion: resolvedRegion,
|
|
374
|
+
onRegionResolved: async (region) => {
|
|
375
|
+
resolvedRegion = region;
|
|
376
|
+
},
|
|
377
|
+
run: async (region) => api.navigate(sessionId, args.url, config, {
|
|
378
|
+
waitUntil: args.options?.waitUntil,
|
|
379
|
+
timeout: args.options?.timeout,
|
|
380
|
+
}, region),
|
|
146
381
|
});
|
|
147
382
|
}
|
|
148
|
-
const result = await
|
|
383
|
+
const result = await runWithRegionRetry(ctx, {
|
|
384
|
+
sessionId,
|
|
385
|
+
initialRegion: resolvedRegion,
|
|
386
|
+
onRegionResolved: async (region) => {
|
|
387
|
+
resolvedRegion = region;
|
|
388
|
+
},
|
|
389
|
+
run: async (region) => api.extract(sessionId, args.instruction, args.schema, config, {
|
|
390
|
+
model: args.model,
|
|
391
|
+
timeout: args.options?.timeout,
|
|
392
|
+
selector: args.options?.selector,
|
|
393
|
+
}, region),
|
|
394
|
+
});
|
|
149
395
|
if (ownSession) {
|
|
150
|
-
await
|
|
396
|
+
await endSessionWithRouting(ctx, {
|
|
397
|
+
sessionId,
|
|
398
|
+
config,
|
|
399
|
+
fallbackRegion: resolvedRegion,
|
|
400
|
+
});
|
|
151
401
|
}
|
|
152
402
|
return result.result;
|
|
153
403
|
}
|
|
154
404
|
catch (error) {
|
|
155
405
|
if (ownSession) {
|
|
156
|
-
await
|
|
406
|
+
await endSessionWithRouting(ctx, {
|
|
407
|
+
sessionId,
|
|
408
|
+
config,
|
|
409
|
+
fallbackRegion: resolvedRegion,
|
|
410
|
+
});
|
|
157
411
|
}
|
|
158
412
|
throw error;
|
|
159
413
|
}
|
|
@@ -174,9 +428,12 @@ export const act = action({
|
|
|
174
428
|
url: v.optional(v.string()),
|
|
175
429
|
action: v.string(),
|
|
176
430
|
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
431
|
+
model: modelValidator,
|
|
432
|
+
sessionConfig: sessionConfigValidator,
|
|
177
433
|
options: v.optional(v.object({
|
|
178
434
|
timeout: v.optional(v.number()),
|
|
179
435
|
waitUntil: v.optional(waitUntilValidator),
|
|
436
|
+
variables: variablesValidator,
|
|
180
437
|
})),
|
|
181
438
|
},
|
|
182
439
|
returns: v.object({
|
|
@@ -184,7 +441,7 @@ export const act = action({
|
|
|
184
441
|
message: v.string(),
|
|
185
442
|
actionDescription: v.string(),
|
|
186
443
|
}),
|
|
187
|
-
handler: async (
|
|
444
|
+
handler: async (ctx, args) => {
|
|
188
445
|
if (!args.sessionId && !args.url) {
|
|
189
446
|
throw new Error("Either sessionId or url must be provided");
|
|
190
447
|
}
|
|
@@ -196,22 +453,63 @@ export const act = action({
|
|
|
196
453
|
};
|
|
197
454
|
const ownSession = !args.sessionId;
|
|
198
455
|
let sessionId = args.sessionId;
|
|
456
|
+
let resolvedRegion;
|
|
199
457
|
if (ownSession) {
|
|
458
|
+
resolvedRegion =
|
|
459
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
460
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
200
461
|
const session = await api.startSession(config, {
|
|
201
462
|
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
463
|
+
model: args.model,
|
|
464
|
+
...args.sessionConfig,
|
|
202
465
|
});
|
|
203
466
|
sessionId = session.sessionId;
|
|
467
|
+
await persistSessionMetadata(ctx, {
|
|
468
|
+
sessionId,
|
|
469
|
+
region: resolvedRegion,
|
|
470
|
+
status: "active",
|
|
471
|
+
operation: "act",
|
|
472
|
+
url: args.url,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!sessionId) {
|
|
476
|
+
throw new Error("Failed to initialize session");
|
|
477
|
+
}
|
|
478
|
+
if (!ownSession) {
|
|
479
|
+
resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
|
|
204
480
|
}
|
|
205
481
|
try {
|
|
206
482
|
if (ownSession && args.url) {
|
|
207
|
-
await
|
|
208
|
-
|
|
209
|
-
|
|
483
|
+
await runWithRegionRetry(ctx, {
|
|
484
|
+
sessionId,
|
|
485
|
+
initialRegion: resolvedRegion,
|
|
486
|
+
onRegionResolved: async (region) => {
|
|
487
|
+
resolvedRegion = region;
|
|
488
|
+
},
|
|
489
|
+
run: async (region) => api.navigate(sessionId, args.url, config, {
|
|
490
|
+
waitUntil: args.options?.waitUntil,
|
|
491
|
+
timeout: args.options?.timeout,
|
|
492
|
+
}, region),
|
|
210
493
|
});
|
|
211
494
|
}
|
|
212
|
-
const result = await
|
|
495
|
+
const result = await runWithRegionRetry(ctx, {
|
|
496
|
+
sessionId,
|
|
497
|
+
initialRegion: resolvedRegion,
|
|
498
|
+
onRegionResolved: async (region) => {
|
|
499
|
+
resolvedRegion = region;
|
|
500
|
+
},
|
|
501
|
+
run: async (region) => api.act(sessionId, args.action, config, {
|
|
502
|
+
model: args.model,
|
|
503
|
+
variables: args.options?.variables,
|
|
504
|
+
timeout: args.options?.timeout,
|
|
505
|
+
}, region),
|
|
506
|
+
});
|
|
213
507
|
if (ownSession) {
|
|
214
|
-
await
|
|
508
|
+
await endSessionWithRouting(ctx, {
|
|
509
|
+
sessionId,
|
|
510
|
+
config,
|
|
511
|
+
fallbackRegion: resolvedRegion,
|
|
512
|
+
});
|
|
215
513
|
}
|
|
216
514
|
return {
|
|
217
515
|
success: result.result.success,
|
|
@@ -221,7 +519,11 @@ export const act = action({
|
|
|
221
519
|
}
|
|
222
520
|
catch (error) {
|
|
223
521
|
if (ownSession) {
|
|
224
|
-
await
|
|
522
|
+
await endSessionWithRouting(ctx, {
|
|
523
|
+
sessionId,
|
|
524
|
+
config,
|
|
525
|
+
fallbackRegion: resolvedRegion,
|
|
526
|
+
});
|
|
225
527
|
}
|
|
226
528
|
throw error;
|
|
227
529
|
}
|
|
@@ -242,13 +544,16 @@ export const observe = action({
|
|
|
242
544
|
url: v.optional(v.string()),
|
|
243
545
|
instruction: v.string(),
|
|
244
546
|
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
547
|
+
model: modelValidator,
|
|
548
|
+
sessionConfig: sessionConfigValidator,
|
|
245
549
|
options: v.optional(v.object({
|
|
246
550
|
timeout: v.optional(v.number()),
|
|
247
551
|
waitUntil: v.optional(waitUntilValidator),
|
|
552
|
+
selector: v.optional(v.string()),
|
|
248
553
|
})),
|
|
249
554
|
},
|
|
250
555
|
returns: v.array(observedActionValidator),
|
|
251
|
-
handler: async (
|
|
556
|
+
handler: async (ctx, args) => {
|
|
252
557
|
if (!args.sessionId && !args.url) {
|
|
253
558
|
throw new Error("Either sessionId or url must be provided");
|
|
254
559
|
}
|
|
@@ -260,33 +565,79 @@ export const observe = action({
|
|
|
260
565
|
};
|
|
261
566
|
const ownSession = !args.sessionId;
|
|
262
567
|
let sessionId = args.sessionId;
|
|
568
|
+
let resolvedRegion;
|
|
263
569
|
if (ownSession) {
|
|
570
|
+
resolvedRegion =
|
|
571
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
572
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
264
573
|
const session = await api.startSession(config, {
|
|
265
574
|
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
575
|
+
model: args.model,
|
|
576
|
+
...args.sessionConfig,
|
|
266
577
|
});
|
|
267
578
|
sessionId = session.sessionId;
|
|
579
|
+
await persistSessionMetadata(ctx, {
|
|
580
|
+
sessionId,
|
|
581
|
+
region: resolvedRegion,
|
|
582
|
+
status: "active",
|
|
583
|
+
operation: "observe",
|
|
584
|
+
url: args.url,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (!sessionId) {
|
|
588
|
+
throw new Error("Failed to initialize session");
|
|
589
|
+
}
|
|
590
|
+
if (!ownSession) {
|
|
591
|
+
resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
|
|
268
592
|
}
|
|
269
593
|
try {
|
|
270
594
|
if (ownSession && args.url) {
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
595
|
+
await runWithRegionRetry(ctx, {
|
|
596
|
+
sessionId,
|
|
597
|
+
initialRegion: resolvedRegion,
|
|
598
|
+
onRegionResolved: async (region) => {
|
|
599
|
+
resolvedRegion = region;
|
|
600
|
+
},
|
|
601
|
+
run: async (region) => api.navigate(sessionId, args.url, config, {
|
|
602
|
+
waitUntil: args.options?.waitUntil,
|
|
603
|
+
timeout: args.options?.timeout,
|
|
604
|
+
}, region),
|
|
274
605
|
});
|
|
275
606
|
}
|
|
276
|
-
const result = await
|
|
607
|
+
const result = await runWithRegionRetry(ctx, {
|
|
608
|
+
sessionId,
|
|
609
|
+
initialRegion: resolvedRegion,
|
|
610
|
+
onRegionResolved: async (region) => {
|
|
611
|
+
resolvedRegion = region;
|
|
612
|
+
},
|
|
613
|
+
run: async (region) => api.observe(sessionId, args.instruction, config, {
|
|
614
|
+
model: args.model,
|
|
615
|
+
timeout: args.options?.timeout,
|
|
616
|
+
selector: args.options?.selector,
|
|
617
|
+
}, region),
|
|
618
|
+
});
|
|
277
619
|
if (ownSession) {
|
|
278
|
-
await
|
|
620
|
+
await endSessionWithRouting(ctx, {
|
|
621
|
+
sessionId,
|
|
622
|
+
config,
|
|
623
|
+
fallbackRegion: resolvedRegion,
|
|
624
|
+
});
|
|
279
625
|
}
|
|
280
626
|
return result.result.map((action) => ({
|
|
281
627
|
description: action.description,
|
|
282
628
|
selector: action.selector,
|
|
283
629
|
method: action.method,
|
|
284
630
|
arguments: action.arguments,
|
|
631
|
+
backendNodeId: action.backendNodeId,
|
|
285
632
|
}));
|
|
286
633
|
}
|
|
287
634
|
catch (error) {
|
|
288
635
|
if (ownSession) {
|
|
289
|
-
await
|
|
636
|
+
await endSessionWithRouting(ctx, {
|
|
637
|
+
sessionId,
|
|
638
|
+
config,
|
|
639
|
+
fallbackRegion: resolvedRegion,
|
|
640
|
+
});
|
|
290
641
|
}
|
|
291
642
|
throw error;
|
|
292
643
|
}
|
|
@@ -308,12 +659,19 @@ export const agent = action({
|
|
|
308
659
|
url: v.optional(v.string()),
|
|
309
660
|
instruction: v.string(),
|
|
310
661
|
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
662
|
+
model: modelValidator,
|
|
663
|
+
sessionConfig: sessionConfigValidator,
|
|
311
664
|
options: v.optional(v.object({
|
|
312
665
|
cua: v.optional(v.boolean()),
|
|
666
|
+
mode: v.optional(v.string()),
|
|
313
667
|
maxSteps: v.optional(v.number()),
|
|
314
668
|
systemPrompt: v.optional(v.string()),
|
|
315
669
|
timeout: v.optional(v.number()),
|
|
316
670
|
waitUntil: v.optional(waitUntilValidator),
|
|
671
|
+
executionModel: modelValidator,
|
|
672
|
+
provider: v.optional(v.string()),
|
|
673
|
+
highlightCursor: v.optional(v.boolean()),
|
|
674
|
+
shouldCache: v.optional(v.boolean()),
|
|
317
675
|
})),
|
|
318
676
|
},
|
|
319
677
|
returns: v.object({
|
|
@@ -321,8 +679,16 @@ export const agent = action({
|
|
|
321
679
|
completed: v.boolean(),
|
|
322
680
|
message: v.string(),
|
|
323
681
|
success: v.boolean(),
|
|
682
|
+
metadata: v.optional(v.any()),
|
|
683
|
+
usage: v.optional(v.object({
|
|
684
|
+
input_tokens: v.number(),
|
|
685
|
+
output_tokens: v.number(),
|
|
686
|
+
reasoning_tokens: v.optional(v.number()),
|
|
687
|
+
cached_input_tokens: v.optional(v.number()),
|
|
688
|
+
inference_time_ms: v.number(),
|
|
689
|
+
})),
|
|
324
690
|
}),
|
|
325
|
-
handler: async (
|
|
691
|
+
handler: async (ctx, args) => {
|
|
326
692
|
if (!args.sessionId && !args.url) {
|
|
327
693
|
throw new Error("Either sessionId or url must be provided");
|
|
328
694
|
}
|
|
@@ -334,34 +700,107 @@ export const agent = action({
|
|
|
334
700
|
};
|
|
335
701
|
const ownSession = !args.sessionId;
|
|
336
702
|
let sessionId = args.sessionId;
|
|
703
|
+
let resolvedRegion;
|
|
337
704
|
if (ownSession) {
|
|
705
|
+
resolvedRegion =
|
|
706
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
707
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
338
708
|
const session = await api.startSession(config, {
|
|
339
709
|
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
710
|
+
model: args.model,
|
|
711
|
+
...args.sessionConfig,
|
|
340
712
|
});
|
|
341
713
|
sessionId = session.sessionId;
|
|
714
|
+
await persistSessionMetadata(ctx, {
|
|
715
|
+
sessionId,
|
|
716
|
+
region: resolvedRegion,
|
|
717
|
+
status: "active",
|
|
718
|
+
operation: "workflow",
|
|
719
|
+
url: args.url,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
if (!sessionId) {
|
|
723
|
+
throw new Error("Failed to initialize session");
|
|
724
|
+
}
|
|
725
|
+
if (!ownSession) {
|
|
726
|
+
resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
|
|
342
727
|
}
|
|
343
728
|
try {
|
|
344
729
|
if (ownSession && args.url) {
|
|
345
|
-
await
|
|
346
|
-
|
|
347
|
-
|
|
730
|
+
await runWithRegionRetry(ctx, {
|
|
731
|
+
sessionId,
|
|
732
|
+
initialRegion: resolvedRegion,
|
|
733
|
+
onRegionResolved: async (region) => {
|
|
734
|
+
resolvedRegion = region;
|
|
735
|
+
},
|
|
736
|
+
run: async (region) => api.navigate(sessionId, args.url, config, {
|
|
737
|
+
waitUntil: args.options?.waitUntil,
|
|
738
|
+
timeout: args.options?.timeout,
|
|
739
|
+
}, region),
|
|
348
740
|
});
|
|
349
741
|
}
|
|
350
|
-
const result = await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
742
|
+
const result = await runWithRegionRetry(ctx, {
|
|
743
|
+
sessionId,
|
|
744
|
+
initialRegion: resolvedRegion,
|
|
745
|
+
onRegionResolved: async (region) => {
|
|
746
|
+
resolvedRegion = region;
|
|
747
|
+
},
|
|
748
|
+
run: async (region) => api.agentExecute(sessionId, {
|
|
749
|
+
cua: args.options?.cua,
|
|
750
|
+
mode: args.options?.mode,
|
|
751
|
+
model: args.model,
|
|
752
|
+
systemPrompt: args.options?.systemPrompt,
|
|
753
|
+
executionModel: args.options?.executionModel,
|
|
754
|
+
provider: args.options?.provider,
|
|
755
|
+
}, {
|
|
756
|
+
instruction: args.instruction,
|
|
757
|
+
maxSteps: args.options?.maxSteps,
|
|
758
|
+
highlightCursor: args.options?.highlightCursor,
|
|
759
|
+
}, config, args.options?.shouldCache, region),
|
|
760
|
+
});
|
|
357
761
|
if (ownSession) {
|
|
358
|
-
await
|
|
762
|
+
await endSessionWithRouting(ctx, {
|
|
763
|
+
sessionId,
|
|
764
|
+
config,
|
|
765
|
+
fallbackRegion: resolvedRegion,
|
|
766
|
+
});
|
|
359
767
|
}
|
|
360
|
-
return
|
|
768
|
+
// Strip passthrough fields to match the Convex return validator.
|
|
769
|
+
// The API may return extra fields (e.g. timestamp, messages) not in the validator.
|
|
770
|
+
const r = result.result;
|
|
771
|
+
return {
|
|
772
|
+
actions: r.actions.map((a) => ({
|
|
773
|
+
type: a.type,
|
|
774
|
+
action: a.action,
|
|
775
|
+
reasoning: a.reasoning,
|
|
776
|
+
timeMs: a.timeMs,
|
|
777
|
+
taskCompleted: a.taskCompleted,
|
|
778
|
+
pageText: a.pageText,
|
|
779
|
+
pageUrl: a.pageUrl,
|
|
780
|
+
instruction: a.instruction,
|
|
781
|
+
})),
|
|
782
|
+
completed: r.completed,
|
|
783
|
+
message: r.message,
|
|
784
|
+
success: r.success,
|
|
785
|
+
metadata: r.metadata,
|
|
786
|
+
usage: r.usage
|
|
787
|
+
? {
|
|
788
|
+
input_tokens: r.usage.input_tokens,
|
|
789
|
+
output_tokens: r.usage.output_tokens,
|
|
790
|
+
reasoning_tokens: r.usage.reasoning_tokens,
|
|
791
|
+
cached_input_tokens: r.usage.cached_input_tokens,
|
|
792
|
+
inference_time_ms: r.usage.inference_time_ms,
|
|
793
|
+
}
|
|
794
|
+
: undefined,
|
|
795
|
+
};
|
|
361
796
|
}
|
|
362
797
|
catch (error) {
|
|
363
798
|
if (ownSession) {
|
|
364
|
-
await
|
|
799
|
+
await endSessionWithRouting(ctx, {
|
|
800
|
+
sessionId,
|
|
801
|
+
config,
|
|
802
|
+
fallbackRegion: resolvedRegion,
|
|
803
|
+
});
|
|
365
804
|
}
|
|
366
805
|
throw error;
|
|
367
806
|
}
|