@browserbasehq/convex-stagehand 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -18
- package/dist/client/index.d.ts +119 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +89 -13
- 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 -16
- package/dist/component/api.d.ts.map +1 -1
- package/dist/component/api.js +94 -33
- package/dist/component/api.js.map +1 -1
- package/dist/component/lib.d.ts +23 -5
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +507 -54
- 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 +1 -1
- package/src/client/index.ts +218 -16
- package/src/component/_generated/component.ts +213 -12
- package/src/component/_generated/dataModel.ts +45 -2
- package/src/component/api.ts +225 -66
- package/src/component/lib.ts +657 -61
- 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,21 +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()),
|
|
212
|
+
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
213
|
+
model: modelValidator,
|
|
35
214
|
options: v.optional(v.object({
|
|
36
215
|
timeout: v.optional(v.number()),
|
|
37
216
|
waitUntil: v.optional(waitUntilValidator),
|
|
38
217
|
domSettleTimeoutMs: v.optional(v.number()),
|
|
39
218
|
selfHeal: v.optional(v.boolean()),
|
|
40
219
|
systemPrompt: v.optional(v.string()),
|
|
220
|
+
verbose: v.optional(v.number()),
|
|
221
|
+
experimental: v.optional(v.boolean()),
|
|
41
222
|
})),
|
|
42
223
|
},
|
|
43
224
|
returns: v.object({
|
|
44
225
|
sessionId: v.string(),
|
|
45
|
-
browserbaseSessionId: v.optional(v.string()),
|
|
46
226
|
cdpUrl: v.optional(v.string()),
|
|
47
227
|
}),
|
|
48
|
-
handler: async (
|
|
228
|
+
handler: async (ctx, args) => {
|
|
229
|
+
let resolvedRegion = getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
230
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
49
231
|
const config = {
|
|
50
232
|
browserbaseApiKey: args.browserbaseApiKey,
|
|
51
233
|
browserbaseProjectId: args.browserbaseProjectId,
|
|
@@ -53,24 +235,45 @@ export const startSession = action({
|
|
|
53
235
|
modelName: args.modelName,
|
|
54
236
|
};
|
|
55
237
|
const session = await api.startSession(config, {
|
|
56
|
-
|
|
238
|
+
browserbaseSessionID: args.browserbaseSessionID,
|
|
239
|
+
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
240
|
+
model: args.model,
|
|
57
241
|
domSettleTimeoutMs: args.options?.domSettleTimeoutMs,
|
|
58
242
|
selfHeal: args.options?.selfHeal,
|
|
59
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,
|
|
60
253
|
});
|
|
61
254
|
try {
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
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),
|
|
65
265
|
});
|
|
66
266
|
return {
|
|
67
267
|
sessionId: session.sessionId,
|
|
68
|
-
|
|
69
|
-
cdpUrl: session.cdpUrl,
|
|
268
|
+
cdpUrl: session.cdpUrl ?? undefined,
|
|
70
269
|
};
|
|
71
270
|
}
|
|
72
271
|
catch (error) {
|
|
73
|
-
await
|
|
272
|
+
await endSessionWithRouting(ctx, {
|
|
273
|
+
sessionId: session.sessionId,
|
|
274
|
+
config,
|
|
275
|
+
fallbackRegion: resolvedRegion,
|
|
276
|
+
});
|
|
74
277
|
throw error;
|
|
75
278
|
}
|
|
76
279
|
},
|
|
@@ -83,17 +286,22 @@ export const endSession = action({
|
|
|
83
286
|
browserbaseApiKey: v.string(),
|
|
84
287
|
browserbaseProjectId: v.string(),
|
|
85
288
|
modelApiKey: v.string(),
|
|
289
|
+
modelName: v.optional(v.string()),
|
|
86
290
|
sessionId: v.string(),
|
|
87
291
|
},
|
|
88
292
|
returns: v.object({ success: v.boolean() }),
|
|
89
|
-
handler: async (
|
|
293
|
+
handler: async (ctx, args) => {
|
|
90
294
|
const config = {
|
|
91
295
|
browserbaseApiKey: args.browserbaseApiKey,
|
|
92
296
|
browserbaseProjectId: args.browserbaseProjectId,
|
|
93
297
|
modelApiKey: args.modelApiKey,
|
|
298
|
+
modelName: args.modelName,
|
|
94
299
|
};
|
|
95
|
-
await
|
|
96
|
-
|
|
300
|
+
const success = await endSessionWithRouting(ctx, {
|
|
301
|
+
sessionId: args.sessionId,
|
|
302
|
+
config,
|
|
303
|
+
});
|
|
304
|
+
return { success };
|
|
97
305
|
},
|
|
98
306
|
});
|
|
99
307
|
/**
|
|
@@ -111,13 +319,17 @@ export const extract = action({
|
|
|
111
319
|
url: v.optional(v.string()),
|
|
112
320
|
instruction: v.string(),
|
|
113
321
|
schema: v.any(),
|
|
322
|
+
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
323
|
+
model: modelValidator,
|
|
324
|
+
sessionConfig: sessionConfigValidator,
|
|
114
325
|
options: v.optional(v.object({
|
|
115
326
|
timeout: v.optional(v.number()),
|
|
116
327
|
waitUntil: v.optional(waitUntilValidator),
|
|
328
|
+
selector: v.optional(v.string()),
|
|
117
329
|
})),
|
|
118
330
|
},
|
|
119
331
|
returns: v.any(),
|
|
120
|
-
handler: async (
|
|
332
|
+
handler: async (ctx, args) => {
|
|
121
333
|
if (!args.sessionId && !args.url) {
|
|
122
334
|
throw new Error("Either sessionId or url must be provided");
|
|
123
335
|
}
|
|
@@ -129,26 +341,73 @@ export const extract = action({
|
|
|
129
341
|
};
|
|
130
342
|
const ownSession = !args.sessionId;
|
|
131
343
|
let sessionId = args.sessionId;
|
|
344
|
+
let resolvedRegion;
|
|
132
345
|
if (ownSession) {
|
|
133
|
-
|
|
346
|
+
resolvedRegion =
|
|
347
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
348
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
349
|
+
const session = await api.startSession(config, {
|
|
350
|
+
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
351
|
+
model: args.model,
|
|
352
|
+
...args.sessionConfig,
|
|
353
|
+
});
|
|
134
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);
|
|
135
368
|
}
|
|
136
369
|
try {
|
|
137
370
|
if (ownSession && args.url) {
|
|
138
|
-
await
|
|
139
|
-
|
|
140
|
-
|
|
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),
|
|
141
381
|
});
|
|
142
382
|
}
|
|
143
|
-
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
|
+
});
|
|
144
395
|
if (ownSession) {
|
|
145
|
-
await
|
|
396
|
+
await endSessionWithRouting(ctx, {
|
|
397
|
+
sessionId,
|
|
398
|
+
config,
|
|
399
|
+
fallbackRegion: resolvedRegion,
|
|
400
|
+
});
|
|
146
401
|
}
|
|
147
402
|
return result.result;
|
|
148
403
|
}
|
|
149
404
|
catch (error) {
|
|
150
405
|
if (ownSession) {
|
|
151
|
-
await
|
|
406
|
+
await endSessionWithRouting(ctx, {
|
|
407
|
+
sessionId,
|
|
408
|
+
config,
|
|
409
|
+
fallbackRegion: resolvedRegion,
|
|
410
|
+
});
|
|
152
411
|
}
|
|
153
412
|
throw error;
|
|
154
413
|
}
|
|
@@ -168,9 +427,13 @@ export const act = action({
|
|
|
168
427
|
sessionId: v.optional(v.string()),
|
|
169
428
|
url: v.optional(v.string()),
|
|
170
429
|
action: v.string(),
|
|
430
|
+
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
431
|
+
model: modelValidator,
|
|
432
|
+
sessionConfig: sessionConfigValidator,
|
|
171
433
|
options: v.optional(v.object({
|
|
172
434
|
timeout: v.optional(v.number()),
|
|
173
435
|
waitUntil: v.optional(waitUntilValidator),
|
|
436
|
+
variables: variablesValidator,
|
|
174
437
|
})),
|
|
175
438
|
},
|
|
176
439
|
returns: v.object({
|
|
@@ -178,7 +441,7 @@ export const act = action({
|
|
|
178
441
|
message: v.string(),
|
|
179
442
|
actionDescription: v.string(),
|
|
180
443
|
}),
|
|
181
|
-
handler: async (
|
|
444
|
+
handler: async (ctx, args) => {
|
|
182
445
|
if (!args.sessionId && !args.url) {
|
|
183
446
|
throw new Error("Either sessionId or url must be provided");
|
|
184
447
|
}
|
|
@@ -190,20 +453,63 @@ export const act = action({
|
|
|
190
453
|
};
|
|
191
454
|
const ownSession = !args.sessionId;
|
|
192
455
|
let sessionId = args.sessionId;
|
|
456
|
+
let resolvedRegion;
|
|
193
457
|
if (ownSession) {
|
|
194
|
-
|
|
458
|
+
resolvedRegion =
|
|
459
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
460
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
461
|
+
const session = await api.startSession(config, {
|
|
462
|
+
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
463
|
+
model: args.model,
|
|
464
|
+
...args.sessionConfig,
|
|
465
|
+
});
|
|
195
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);
|
|
196
480
|
}
|
|
197
481
|
try {
|
|
198
482
|
if (ownSession && args.url) {
|
|
199
|
-
await
|
|
200
|
-
|
|
201
|
-
|
|
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),
|
|
202
493
|
});
|
|
203
494
|
}
|
|
204
|
-
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
|
+
});
|
|
205
507
|
if (ownSession) {
|
|
206
|
-
await
|
|
508
|
+
await endSessionWithRouting(ctx, {
|
|
509
|
+
sessionId,
|
|
510
|
+
config,
|
|
511
|
+
fallbackRegion: resolvedRegion,
|
|
512
|
+
});
|
|
207
513
|
}
|
|
208
514
|
return {
|
|
209
515
|
success: result.result.success,
|
|
@@ -213,7 +519,11 @@ export const act = action({
|
|
|
213
519
|
}
|
|
214
520
|
catch (error) {
|
|
215
521
|
if (ownSession) {
|
|
216
|
-
await
|
|
522
|
+
await endSessionWithRouting(ctx, {
|
|
523
|
+
sessionId,
|
|
524
|
+
config,
|
|
525
|
+
fallbackRegion: resolvedRegion,
|
|
526
|
+
});
|
|
217
527
|
}
|
|
218
528
|
throw error;
|
|
219
529
|
}
|
|
@@ -233,13 +543,17 @@ export const observe = action({
|
|
|
233
543
|
sessionId: v.optional(v.string()),
|
|
234
544
|
url: v.optional(v.string()),
|
|
235
545
|
instruction: v.string(),
|
|
546
|
+
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
547
|
+
model: modelValidator,
|
|
548
|
+
sessionConfig: sessionConfigValidator,
|
|
236
549
|
options: v.optional(v.object({
|
|
237
550
|
timeout: v.optional(v.number()),
|
|
238
551
|
waitUntil: v.optional(waitUntilValidator),
|
|
552
|
+
selector: v.optional(v.string()),
|
|
239
553
|
})),
|
|
240
554
|
},
|
|
241
555
|
returns: v.array(observedActionValidator),
|
|
242
|
-
handler: async (
|
|
556
|
+
handler: async (ctx, args) => {
|
|
243
557
|
if (!args.sessionId && !args.url) {
|
|
244
558
|
throw new Error("Either sessionId or url must be provided");
|
|
245
559
|
}
|
|
@@ -251,31 +565,79 @@ export const observe = action({
|
|
|
251
565
|
};
|
|
252
566
|
const ownSession = !args.sessionId;
|
|
253
567
|
let sessionId = args.sessionId;
|
|
568
|
+
let resolvedRegion;
|
|
254
569
|
if (ownSession) {
|
|
255
|
-
|
|
570
|
+
resolvedRegion =
|
|
571
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
572
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
573
|
+
const session = await api.startSession(config, {
|
|
574
|
+
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
575
|
+
model: args.model,
|
|
576
|
+
...args.sessionConfig,
|
|
577
|
+
});
|
|
256
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);
|
|
257
592
|
}
|
|
258
593
|
try {
|
|
259
594
|
if (ownSession && args.url) {
|
|
260
|
-
await
|
|
261
|
-
|
|
262
|
-
|
|
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),
|
|
263
605
|
});
|
|
264
606
|
}
|
|
265
|
-
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
|
+
});
|
|
266
619
|
if (ownSession) {
|
|
267
|
-
await
|
|
620
|
+
await endSessionWithRouting(ctx, {
|
|
621
|
+
sessionId,
|
|
622
|
+
config,
|
|
623
|
+
fallbackRegion: resolvedRegion,
|
|
624
|
+
});
|
|
268
625
|
}
|
|
269
626
|
return result.result.map((action) => ({
|
|
270
627
|
description: action.description,
|
|
271
628
|
selector: action.selector,
|
|
272
629
|
method: action.method,
|
|
273
630
|
arguments: action.arguments,
|
|
631
|
+
backendNodeId: action.backendNodeId,
|
|
274
632
|
}));
|
|
275
633
|
}
|
|
276
634
|
catch (error) {
|
|
277
635
|
if (ownSession) {
|
|
278
|
-
await
|
|
636
|
+
await endSessionWithRouting(ctx, {
|
|
637
|
+
sessionId,
|
|
638
|
+
config,
|
|
639
|
+
fallbackRegion: resolvedRegion,
|
|
640
|
+
});
|
|
279
641
|
}
|
|
280
642
|
throw error;
|
|
281
643
|
}
|
|
@@ -296,12 +658,20 @@ export const agent = action({
|
|
|
296
658
|
sessionId: v.optional(v.string()),
|
|
297
659
|
url: v.optional(v.string()),
|
|
298
660
|
instruction: v.string(),
|
|
661
|
+
browserbaseSessionCreateParams: v.optional(v.any()),
|
|
662
|
+
model: modelValidator,
|
|
663
|
+
sessionConfig: sessionConfigValidator,
|
|
299
664
|
options: v.optional(v.object({
|
|
300
665
|
cua: v.optional(v.boolean()),
|
|
666
|
+
mode: v.optional(v.string()),
|
|
301
667
|
maxSteps: v.optional(v.number()),
|
|
302
668
|
systemPrompt: v.optional(v.string()),
|
|
303
669
|
timeout: v.optional(v.number()),
|
|
304
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()),
|
|
305
675
|
})),
|
|
306
676
|
},
|
|
307
677
|
returns: v.object({
|
|
@@ -309,8 +679,16 @@ export const agent = action({
|
|
|
309
679
|
completed: v.boolean(),
|
|
310
680
|
message: v.string(),
|
|
311
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
|
+
})),
|
|
312
690
|
}),
|
|
313
|
-
handler: async (
|
|
691
|
+
handler: async (ctx, args) => {
|
|
314
692
|
if (!args.sessionId && !args.url) {
|
|
315
693
|
throw new Error("Either sessionId or url must be provided");
|
|
316
694
|
}
|
|
@@ -322,32 +700,107 @@ export const agent = action({
|
|
|
322
700
|
};
|
|
323
701
|
const ownSession = !args.sessionId;
|
|
324
702
|
let sessionId = args.sessionId;
|
|
703
|
+
let resolvedRegion;
|
|
325
704
|
if (ownSession) {
|
|
326
|
-
|
|
705
|
+
resolvedRegion =
|
|
706
|
+
getRequestedRegion(args.browserbaseSessionCreateParams) ??
|
|
707
|
+
DEFAULT_BROWSERBASE_REGION;
|
|
708
|
+
const session = await api.startSession(config, {
|
|
709
|
+
browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
|
|
710
|
+
model: args.model,
|
|
711
|
+
...args.sessionConfig,
|
|
712
|
+
});
|
|
327
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);
|
|
328
727
|
}
|
|
329
728
|
try {
|
|
330
729
|
if (ownSession && args.url) {
|
|
331
|
-
await
|
|
332
|
-
|
|
333
|
-
|
|
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),
|
|
334
740
|
});
|
|
335
741
|
}
|
|
336
|
-
const result = await
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
});
|
|
343
761
|
if (ownSession) {
|
|
344
|
-
await
|
|
762
|
+
await endSessionWithRouting(ctx, {
|
|
763
|
+
sessionId,
|
|
764
|
+
config,
|
|
765
|
+
fallbackRegion: resolvedRegion,
|
|
766
|
+
});
|
|
345
767
|
}
|
|
346
|
-
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
|
+
};
|
|
347
796
|
}
|
|
348
797
|
catch (error) {
|
|
349
798
|
if (ownSession) {
|
|
350
|
-
await
|
|
799
|
+
await endSessionWithRouting(ctx, {
|
|
800
|
+
sessionId,
|
|
801
|
+
config,
|
|
802
|
+
fallbackRegion: resolvedRegion,
|
|
803
|
+
});
|
|
351
804
|
}
|
|
352
805
|
throw error;
|
|
353
806
|
}
|