@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.
@@ -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
- browserbaseSessionId: v.optional(v.string()),
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 (_ctx, args) => {
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
- browserbaseSessionId: args.browserbaseSessionId,
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 api.navigate(session.sessionId, args.url, config, {
63
- waitUntil: args.options?.waitUntil,
64
- timeout: args.options?.timeout,
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
- browserbaseSessionId: session.browserbaseSessionId,
69
- cdpUrl: session.cdpUrl,
268
+ cdpUrl: session.cdpUrl ?? undefined,
70
269
  };
71
270
  }
72
271
  catch (error) {
73
- await api.endSession(session.sessionId, config);
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 (_ctx, args) => {
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 api.endSession(args.sessionId, config);
96
- return { success: true };
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 (_ctx, args) => {
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
- const session = await api.startSession(config);
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 api.navigate(sessionId, args.url, config, {
139
- waitUntil: args.options?.waitUntil,
140
- timeout: args.options?.timeout,
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 api.extract(sessionId, args.instruction, args.schema, config);
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 api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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
- const session = await api.startSession(config);
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 api.navigate(sessionId, args.url, config, {
200
- waitUntil: args.options?.waitUntil,
201
- timeout: args.options?.timeout,
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 api.act(sessionId, args.action, config);
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 api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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
- const session = await api.startSession(config);
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 api.navigate(sessionId, args.url, config, {
261
- waitUntil: args.options?.waitUntil,
262
- timeout: args.options?.timeout,
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 api.observe(sessionId, args.instruction, config);
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 api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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
- const session = await api.startSession(config);
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 api.navigate(sessionId, args.url, config, {
332
- waitUntil: args.options?.waitUntil,
333
- timeout: args.options?.timeout,
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 api.agentExecute(sessionId, {
337
- cua: args.options?.cua,
338
- systemPrompt: args.options?.systemPrompt,
339
- }, {
340
- instruction: args.instruction,
341
- maxSteps: args.options?.maxSteps,
342
- }, config);
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 api.endSession(sessionId, config);
762
+ await endSessionWithRouting(ctx, {
763
+ sessionId,
764
+ config,
765
+ fallbackRegion: resolvedRegion,
766
+ });
345
767
  }
346
- return result.result;
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 api.endSession(sessionId, config);
799
+ await endSessionWithRouting(ctx, {
800
+ sessionId,
801
+ config,
802
+ fallbackRegion: resolvedRegion,
803
+ });
351
804
  }
352
805
  throw error;
353
806
  }