@browserbasehq/convex-stagehand 0.0.3 → 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,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
- browserbaseSessionId: v.optional(v.string()),
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 (_ctx, args) => {
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
- browserbaseSessionId: args.browserbaseSessionId,
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 api.navigate(session.sessionId, args.url, config, {
65
- waitUntil: args.options?.waitUntil,
66
- 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),
67
265
  });
68
266
  return {
69
267
  sessionId: session.sessionId,
70
- browserbaseSessionId: session.browserbaseSessionId,
71
- cdpUrl: session.cdpUrl,
268
+ cdpUrl: session.cdpUrl ?? undefined,
72
269
  };
73
270
  }
74
271
  catch (error) {
75
- await api.endSession(session.sessionId, config);
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 (_ctx, args) => {
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 api.endSession(args.sessionId, config);
98
- return { success: true };
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 (_ctx, args) => {
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 api.navigate(sessionId, args.url, config, {
144
- waitUntil: args.options?.waitUntil,
145
- 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),
146
381
  });
147
382
  }
148
- 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
+ });
149
395
  if (ownSession) {
150
- await api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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 api.navigate(sessionId, args.url, config, {
208
- waitUntil: args.options?.waitUntil,
209
- 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),
210
493
  });
211
494
  }
212
- 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
+ });
213
507
  if (ownSession) {
214
- await api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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 api.navigate(sessionId, args.url, config, {
272
- waitUntil: args.options?.waitUntil,
273
- 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),
274
605
  });
275
606
  }
276
- 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
+ });
277
619
  if (ownSession) {
278
- await api.endSession(sessionId, config);
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 api.endSession(sessionId, config);
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 (_ctx, args) => {
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 api.navigate(sessionId, args.url, config, {
346
- waitUntil: args.options?.waitUntil,
347
- 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),
348
740
  });
349
741
  }
350
- const result = await api.agentExecute(sessionId, {
351
- cua: args.options?.cua,
352
- systemPrompt: args.options?.systemPrompt,
353
- }, {
354
- instruction: args.instruction,
355
- maxSteps: args.options?.maxSteps,
356
- }, 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
+ });
357
761
  if (ownSession) {
358
- await api.endSession(sessionId, config);
762
+ await endSessionWithRouting(ctx, {
763
+ sessionId,
764
+ config,
765
+ fallbackRegion: resolvedRegion,
766
+ });
359
767
  }
360
- 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
+ };
361
796
  }
362
797
  catch (error) {
363
798
  if (ownSession) {
364
- await api.endSession(sessionId, config);
799
+ await endSessionWithRouting(ctx, {
800
+ sessionId,
801
+ config,
802
+ fallbackRegion: resolvedRegion,
803
+ });
365
804
  }
366
805
  throw error;
367
806
  }