@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.
@@ -5,15 +5,52 @@
5
5
  * Supports both automatic session management and manual session control.
6
6
  */
7
7
 
8
- import { action } from "./_generated/server.js";
8
+ import { action, internalMutation, internalQuery } from "./_generated/server.js";
9
+ import { internal } from "./_generated/api.js";
9
10
  import { v } from "convex/values";
10
11
  import * as api from "./api.js";
11
12
 
13
+ const DEFAULT_BROWSERBASE_REGION: api.BrowserbaseRegion = "us-west-2";
14
+
15
+ type SessionStatus = "active" | "completed" | "error";
16
+ type SessionOperation = "extract" | "act" | "observe" | "workflow";
17
+
18
+ type SessionMetadataPatch = {
19
+ sessionId: string;
20
+ region?: api.BrowserbaseRegion;
21
+ status?: SessionStatus;
22
+ operation?: SessionOperation;
23
+ url?: string;
24
+ endedAt?: number;
25
+ error?: string;
26
+ };
27
+
28
+ const browserbaseRegionValidator = v.union(
29
+ v.literal("us-west-2"),
30
+ v.literal("us-east-1"),
31
+ v.literal("eu-central-1"),
32
+ v.literal("ap-southeast-1"),
33
+ );
34
+
35
+ const sessionStatusValidator = v.union(
36
+ v.literal("active"),
37
+ v.literal("completed"),
38
+ v.literal("error"),
39
+ );
40
+
41
+ const sessionOperationValidator = v.union(
42
+ v.literal("extract"),
43
+ v.literal("act"),
44
+ v.literal("observe"),
45
+ v.literal("workflow"),
46
+ );
47
+
12
48
  const observedActionValidator = v.object({
13
49
  description: v.string(),
14
50
  selector: v.string(),
15
- method: v.string(),
51
+ method: v.optional(v.string()),
16
52
  arguments: v.optional(v.array(v.string())),
53
+ backendNodeId: v.optional(v.number()),
17
54
  });
18
55
 
19
56
  const waitUntilValidator = v.union(
@@ -22,13 +59,228 @@ const waitUntilValidator = v.union(
22
59
  v.literal("networkidle"),
23
60
  );
24
61
 
62
+ /** Session-level config forwarded from the client's StagehandConfig for ephemeral sessions. */
63
+ const sessionConfigValidator = v.optional(
64
+ v.object({
65
+ domSettleTimeoutMs: v.optional(v.number()),
66
+ selfHeal: v.optional(v.boolean()),
67
+ systemPrompt: v.optional(v.string()),
68
+ verbose: v.optional(v.number()),
69
+ experimental: v.optional(v.boolean()),
70
+ }),
71
+ );
72
+
73
+ const modelValidator = v.optional(
74
+ v.union(
75
+ v.string(),
76
+ v.object({
77
+ modelName: v.optional(v.string()),
78
+ apiKey: v.optional(v.string()),
79
+ baseURL: v.optional(v.string()),
80
+ provider: v.optional(v.string()),
81
+ }),
82
+ ),
83
+ );
84
+
85
+ const variablesValidator = v.optional(v.record(v.string(), v.string()));
86
+
25
87
  const agentActionValidator = v.object({
26
88
  type: v.string(),
27
89
  action: v.optional(v.string()),
28
90
  reasoning: v.optional(v.string()),
29
91
  timeMs: v.optional(v.number()),
92
+ taskCompleted: v.optional(v.boolean()),
93
+ pageText: v.optional(v.string()),
94
+ pageUrl: v.optional(v.string()),
95
+ instruction: v.optional(v.string()),
96
+ });
97
+
98
+ function isBrowserbaseRegion(value: unknown): value is api.BrowserbaseRegion {
99
+ return (
100
+ value === "us-west-2" ||
101
+ value === "us-east-1" ||
102
+ value === "eu-central-1" ||
103
+ value === "ap-southeast-1"
104
+ );
105
+ }
106
+
107
+ function getRequestedRegion(
108
+ browserbaseSessionCreateParams: unknown,
109
+ ): api.BrowserbaseRegion | undefined {
110
+ const maybeRegion = (
111
+ browserbaseSessionCreateParams as api.BrowserbaseSessionCreateParams | undefined
112
+ )?.region;
113
+ if (isBrowserbaseRegion(maybeRegion)) {
114
+ return maybeRegion;
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ function extractRegionFromError(error: unknown): api.BrowserbaseRegion | undefined {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ const match = message.match(/Session is in region '([^']+)'/i);
122
+ const parsedRegion = match?.[1];
123
+ if (isBrowserbaseRegion(parsedRegion)) {
124
+ return parsedRegion;
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ export const upsertSessionMetadata = internalMutation({
130
+ args: {
131
+ sessionId: v.string(),
132
+ region: v.optional(browserbaseRegionValidator),
133
+ status: v.optional(sessionStatusValidator),
134
+ operation: v.optional(sessionOperationValidator),
135
+ url: v.optional(v.string()),
136
+ endedAt: v.optional(v.number()),
137
+ error: v.optional(v.string()),
138
+ },
139
+ returns: v.null(),
140
+ handler: async (ctx: any, args: any) => {
141
+ const existing = await ctx.db
142
+ .query("sessions")
143
+ .withIndex("by_sessionId", (q: any) => q.eq("sessionId", args.sessionId))
144
+ .first();
145
+
146
+ if (existing) {
147
+ const patch: Record<string, unknown> = {};
148
+ if (args.region !== undefined) patch.region = args.region;
149
+ if (args.status !== undefined) patch.status = args.status;
150
+ if (args.operation !== undefined) patch.operation = args.operation;
151
+ if (args.url !== undefined) patch.url = args.url;
152
+ if (args.endedAt !== undefined) patch.endedAt = args.endedAt;
153
+ if (args.error !== undefined) patch.error = args.error;
154
+ await ctx.db.patch(existing._id, patch);
155
+ return null;
156
+ }
157
+
158
+ await ctx.db.insert("sessions", {
159
+ sessionId: args.sessionId,
160
+ region: args.region,
161
+ startedAt: Date.now(),
162
+ endedAt: args.endedAt,
163
+ status: args.status ?? "active",
164
+ operation: args.operation ?? "workflow",
165
+ url: args.url ?? "",
166
+ error: args.error,
167
+ });
168
+ return null;
169
+ },
170
+ });
171
+
172
+ export const getSessionRegion = internalQuery({
173
+ args: {
174
+ sessionId: v.string(),
175
+ },
176
+ returns: v.union(browserbaseRegionValidator, v.null()),
177
+ handler: async (ctx: any, args: any) => {
178
+ const session = await ctx.db
179
+ .query("sessions")
180
+ .withIndex("by_sessionId", (q: any) => q.eq("sessionId", args.sessionId))
181
+ .first();
182
+ return session?.region ?? null;
183
+ },
30
184
  });
31
185
 
186
+ async function resolveSessionRegion(
187
+ ctx: any,
188
+ sessionId: string,
189
+ fallback?: api.BrowserbaseRegion,
190
+ ): Promise<api.BrowserbaseRegion | undefined> {
191
+ const storedRegion = await ctx.runQuery(internal.lib.getSessionRegion, {
192
+ sessionId,
193
+ });
194
+ return storedRegion ?? fallback ?? undefined;
195
+ }
196
+
197
+ async function persistSessionMetadata(
198
+ ctx: any,
199
+ args: SessionMetadataPatch,
200
+ ): Promise<void> {
201
+ await ctx.runMutation(internal.lib.upsertSessionMetadata, args);
202
+ }
203
+
204
+ async function runWithRegionRetry<T>(
205
+ ctx: any,
206
+ args: {
207
+ sessionId: string;
208
+ initialRegion?: api.BrowserbaseRegion;
209
+ run: (region?: api.BrowserbaseRegion) => Promise<T>;
210
+ onRegionResolved?: (region: api.BrowserbaseRegion) => Promise<void>;
211
+ },
212
+ ): Promise<T> {
213
+ try {
214
+ return await args.run(args.initialRegion);
215
+ } catch (error) {
216
+ const parsedRegion = extractRegionFromError(error);
217
+ if (!parsedRegion || parsedRegion === args.initialRegion) {
218
+ throw error;
219
+ }
220
+
221
+ await persistSessionMetadata(ctx, {
222
+ sessionId: args.sessionId,
223
+ region: parsedRegion,
224
+ status: "active",
225
+ });
226
+ if (args.onRegionResolved) {
227
+ await args.onRegionResolved(parsedRegion);
228
+ }
229
+
230
+ return args.run(parsedRegion);
231
+ }
232
+ }
233
+
234
+ async function endSessionWithRouting(
235
+ ctx: any,
236
+ args: {
237
+ sessionId: string;
238
+ config: api.ApiConfig;
239
+ fallbackRegion?: api.BrowserbaseRegion;
240
+ },
241
+ ): Promise<boolean> {
242
+ try {
243
+ let resolvedRegion =
244
+ (await resolveSessionRegion(ctx, args.sessionId, args.fallbackRegion)) ??
245
+ DEFAULT_BROWSERBASE_REGION;
246
+
247
+ try {
248
+ await runWithRegionRetry(ctx, {
249
+ sessionId: args.sessionId,
250
+ initialRegion: resolvedRegion,
251
+ onRegionResolved: async (region) => {
252
+ resolvedRegion = region;
253
+ },
254
+ run: async (region) => {
255
+ await api.endSession(args.sessionId, args.config, region);
256
+ },
257
+ });
258
+
259
+ await persistSessionMetadata(ctx, {
260
+ sessionId: args.sessionId,
261
+ region: resolvedRegion,
262
+ status: "completed",
263
+ endedAt: Date.now(),
264
+ });
265
+ return true;
266
+ } catch {
267
+ try {
268
+ await persistSessionMetadata(ctx, {
269
+ sessionId: args.sessionId,
270
+ region: resolvedRegion,
271
+ status: "error",
272
+ error: "Failed to end Stagehand session",
273
+ });
274
+ } catch {
275
+ // Best-effort metadata persistence — don't mask the original failure.
276
+ }
277
+ return false;
278
+ }
279
+ } catch {
280
+ return false;
281
+ }
282
+ }
283
+
32
284
  /**
33
285
  * Start a new browser session.
34
286
  * Returns session info including cdpUrl for direct Playwright/Puppeteer connection.
@@ -40,8 +292,9 @@ export const startSession = action({
40
292
  modelApiKey: v.string(),
41
293
  modelName: v.optional(v.string()),
42
294
  url: v.string(),
43
- browserbaseSessionId: v.optional(v.string()),
295
+ browserbaseSessionID: v.optional(v.string()),
44
296
  browserbaseSessionCreateParams: v.optional(v.any()),
297
+ model: modelValidator,
45
298
  options: v.optional(
46
299
  v.object({
47
300
  timeout: v.optional(v.number()),
@@ -49,15 +302,20 @@ export const startSession = action({
49
302
  domSettleTimeoutMs: v.optional(v.number()),
50
303
  selfHeal: v.optional(v.boolean()),
51
304
  systemPrompt: v.optional(v.string()),
305
+ verbose: v.optional(v.number()),
306
+ experimental: v.optional(v.boolean()),
52
307
  }),
53
308
  ),
54
309
  },
55
310
  returns: v.object({
56
311
  sessionId: v.string(),
57
- browserbaseSessionId: v.optional(v.string()),
58
312
  cdpUrl: v.optional(v.string()),
59
313
  }),
60
- handler: async (_ctx: any, args: any) => {
314
+ handler: async (ctx: any, args: any) => {
315
+ let resolvedRegion =
316
+ getRequestedRegion(args.browserbaseSessionCreateParams) ??
317
+ DEFAULT_BROWSERBASE_REGION;
318
+
61
319
  const config: api.ApiConfig = {
62
320
  browserbaseApiKey: args.browserbaseApiKey,
63
321
  browserbaseProjectId: args.browserbaseProjectId,
@@ -66,26 +324,54 @@ export const startSession = action({
66
324
  };
67
325
 
68
326
  const session = await api.startSession(config, {
69
- browserbaseSessionId: args.browserbaseSessionId,
327
+ browserbaseSessionID: args.browserbaseSessionID,
70
328
  browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
329
+ model: args.model,
71
330
  domSettleTimeoutMs: args.options?.domSettleTimeoutMs,
72
331
  selfHeal: args.options?.selfHeal,
73
332
  systemPrompt: args.options?.systemPrompt,
333
+ verbose: args.options?.verbose,
334
+ experimental: args.options?.experimental,
335
+ });
336
+
337
+ await persistSessionMetadata(ctx, {
338
+ sessionId: session.sessionId,
339
+ region: resolvedRegion,
340
+ status: "active",
341
+ operation: "workflow",
342
+ url: args.url,
74
343
  });
75
344
 
76
345
  try {
77
- await api.navigate(session.sessionId, args.url, config, {
78
- waitUntil: args.options?.waitUntil,
79
- timeout: args.options?.timeout,
346
+ await runWithRegionRetry(ctx, {
347
+ sessionId: session.sessionId,
348
+ initialRegion: resolvedRegion,
349
+ onRegionResolved: async (region) => {
350
+ resolvedRegion = region;
351
+ },
352
+ run: async (region) =>
353
+ api.navigate(
354
+ session.sessionId,
355
+ args.url,
356
+ config,
357
+ {
358
+ waitUntil: args.options?.waitUntil,
359
+ timeout: args.options?.timeout,
360
+ },
361
+ region,
362
+ ),
80
363
  });
81
364
 
82
365
  return {
83
366
  sessionId: session.sessionId,
84
- browserbaseSessionId: session.browserbaseSessionId,
85
- cdpUrl: session.cdpUrl,
367
+ cdpUrl: session.cdpUrl ?? undefined,
86
368
  };
87
369
  } catch (error) {
88
- await api.endSession(session.sessionId, config);
370
+ await endSessionWithRouting(ctx, {
371
+ sessionId: session.sessionId,
372
+ config,
373
+ fallbackRegion: resolvedRegion,
374
+ });
89
375
  throw error;
90
376
  }
91
377
  },
@@ -99,18 +385,23 @@ export const endSession = action({
99
385
  browserbaseApiKey: v.string(),
100
386
  browserbaseProjectId: v.string(),
101
387
  modelApiKey: v.string(),
388
+ modelName: v.optional(v.string()),
102
389
  sessionId: v.string(),
103
390
  },
104
391
  returns: v.object({ success: v.boolean() }),
105
- handler: async (_ctx: any, args: any) => {
392
+ handler: async (ctx: any, args: any) => {
106
393
  const config: api.ApiConfig = {
107
394
  browserbaseApiKey: args.browserbaseApiKey,
108
395
  browserbaseProjectId: args.browserbaseProjectId,
109
396
  modelApiKey: args.modelApiKey,
397
+ modelName: args.modelName,
110
398
  };
111
399
 
112
- await api.endSession(args.sessionId, config);
113
- return { success: true };
400
+ const success = await endSessionWithRouting(ctx, {
401
+ sessionId: args.sessionId,
402
+ config,
403
+ });
404
+ return { success };
114
405
  },
115
406
  });
116
407
 
@@ -130,15 +421,18 @@ export const extract = action({
130
421
  instruction: v.string(),
131
422
  schema: v.any(),
132
423
  browserbaseSessionCreateParams: v.optional(v.any()),
424
+ model: modelValidator,
425
+ sessionConfig: sessionConfigValidator,
133
426
  options: v.optional(
134
427
  v.object({
135
428
  timeout: v.optional(v.number()),
136
429
  waitUntil: v.optional(waitUntilValidator),
430
+ selector: v.optional(v.string()),
137
431
  }),
138
432
  ),
139
433
  },
140
434
  returns: v.any(),
141
- handler: async (_ctx: any, args: any) => {
435
+ handler: async (ctx: any, args: any) => {
142
436
  if (!args.sessionId && !args.url) {
143
437
  throw new Error("Either sessionId or url must be provided");
144
438
  }
@@ -152,37 +446,94 @@ export const extract = action({
152
446
 
153
447
  const ownSession = !args.sessionId;
154
448
  let sessionId = args.sessionId;
449
+ let resolvedRegion: api.BrowserbaseRegion | undefined;
155
450
 
156
451
  if (ownSession) {
452
+ resolvedRegion =
453
+ getRequestedRegion(args.browserbaseSessionCreateParams) ??
454
+ DEFAULT_BROWSERBASE_REGION;
157
455
  const session = await api.startSession(config, {
158
456
  browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
457
+ model: args.model,
458
+ ...args.sessionConfig,
159
459
  });
160
460
  sessionId = session.sessionId;
461
+ await persistSessionMetadata(ctx, {
462
+ sessionId,
463
+ region: resolvedRegion,
464
+ status: "active",
465
+ operation: "extract",
466
+ url: args.url,
467
+ });
468
+ }
469
+
470
+ if (!sessionId) {
471
+ throw new Error("Failed to initialize session");
472
+ }
473
+
474
+ if (!ownSession) {
475
+ resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
161
476
  }
162
477
 
163
478
  try {
164
479
  if (ownSession && args.url) {
165
- await api.navigate(sessionId, args.url, config, {
166
- waitUntil: args.options?.waitUntil,
167
- timeout: args.options?.timeout,
480
+ await runWithRegionRetry(ctx, {
481
+ sessionId,
482
+ initialRegion: resolvedRegion,
483
+ onRegionResolved: async (region) => {
484
+ resolvedRegion = region;
485
+ },
486
+ run: async (region) =>
487
+ api.navigate(
488
+ sessionId,
489
+ args.url,
490
+ config,
491
+ {
492
+ waitUntil: args.options?.waitUntil,
493
+ timeout: args.options?.timeout,
494
+ },
495
+ region,
496
+ ),
168
497
  });
169
498
  }
170
499
 
171
- const result = await api.extract(
500
+ const result = await runWithRegionRetry(ctx, {
172
501
  sessionId,
173
- args.instruction,
174
- args.schema,
175
- config,
176
- );
502
+ initialRegion: resolvedRegion,
503
+ onRegionResolved: async (region) => {
504
+ resolvedRegion = region;
505
+ },
506
+ run: async (region) =>
507
+ api.extract(
508
+ sessionId,
509
+ args.instruction,
510
+ args.schema,
511
+ config,
512
+ {
513
+ model: args.model,
514
+ timeout: args.options?.timeout,
515
+ selector: args.options?.selector,
516
+ },
517
+ region,
518
+ ),
519
+ });
177
520
 
178
521
  if (ownSession) {
179
- await api.endSession(sessionId, config);
522
+ await endSessionWithRouting(ctx, {
523
+ sessionId,
524
+ config,
525
+ fallbackRegion: resolvedRegion,
526
+ });
180
527
  }
181
528
 
182
529
  return result.result;
183
530
  } catch (error) {
184
531
  if (ownSession) {
185
- await api.endSession(sessionId, config);
532
+ await endSessionWithRouting(ctx, {
533
+ sessionId,
534
+ config,
535
+ fallbackRegion: resolvedRegion,
536
+ });
186
537
  }
187
538
  throw error;
188
539
  }
@@ -204,10 +555,13 @@ export const act = action({
204
555
  url: v.optional(v.string()),
205
556
  action: v.string(),
206
557
  browserbaseSessionCreateParams: v.optional(v.any()),
558
+ model: modelValidator,
559
+ sessionConfig: sessionConfigValidator,
207
560
  options: v.optional(
208
561
  v.object({
209
562
  timeout: v.optional(v.number()),
210
563
  waitUntil: v.optional(waitUntilValidator),
564
+ variables: variablesValidator,
211
565
  }),
212
566
  ),
213
567
  },
@@ -216,7 +570,7 @@ export const act = action({
216
570
  message: v.string(),
217
571
  actionDescription: v.string(),
218
572
  }),
219
- handler: async (_ctx: any, args: any) => {
573
+ handler: async (ctx: any, args: any) => {
220
574
  if (!args.sessionId && !args.url) {
221
575
  throw new Error("Either sessionId or url must be provided");
222
576
  }
@@ -230,26 +584,83 @@ export const act = action({
230
584
 
231
585
  const ownSession = !args.sessionId;
232
586
  let sessionId = args.sessionId;
587
+ let resolvedRegion: api.BrowserbaseRegion | undefined;
233
588
 
234
589
  if (ownSession) {
590
+ resolvedRegion =
591
+ getRequestedRegion(args.browserbaseSessionCreateParams) ??
592
+ DEFAULT_BROWSERBASE_REGION;
235
593
  const session = await api.startSession(config, {
236
594
  browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
595
+ model: args.model,
596
+ ...args.sessionConfig,
237
597
  });
238
598
  sessionId = session.sessionId;
599
+ await persistSessionMetadata(ctx, {
600
+ sessionId,
601
+ region: resolvedRegion,
602
+ status: "active",
603
+ operation: "act",
604
+ url: args.url,
605
+ });
606
+ }
607
+
608
+ if (!sessionId) {
609
+ throw new Error("Failed to initialize session");
610
+ }
611
+
612
+ if (!ownSession) {
613
+ resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
239
614
  }
240
615
 
241
616
  try {
242
617
  if (ownSession && args.url) {
243
- await api.navigate(sessionId, args.url, config, {
244
- waitUntil: args.options?.waitUntil,
245
- timeout: args.options?.timeout,
618
+ await runWithRegionRetry(ctx, {
619
+ sessionId,
620
+ initialRegion: resolvedRegion,
621
+ onRegionResolved: async (region) => {
622
+ resolvedRegion = region;
623
+ },
624
+ run: async (region) =>
625
+ api.navigate(
626
+ sessionId,
627
+ args.url,
628
+ config,
629
+ {
630
+ waitUntil: args.options?.waitUntil,
631
+ timeout: args.options?.timeout,
632
+ },
633
+ region,
634
+ ),
246
635
  });
247
636
  }
248
637
 
249
- const result = await api.act(sessionId, args.action, config);
638
+ const result = await runWithRegionRetry(ctx, {
639
+ sessionId,
640
+ initialRegion: resolvedRegion,
641
+ onRegionResolved: async (region) => {
642
+ resolvedRegion = region;
643
+ },
644
+ run: async (region) =>
645
+ api.act(
646
+ sessionId,
647
+ args.action,
648
+ config,
649
+ {
650
+ model: args.model,
651
+ variables: args.options?.variables,
652
+ timeout: args.options?.timeout,
653
+ },
654
+ region,
655
+ ),
656
+ });
250
657
 
251
658
  if (ownSession) {
252
- await api.endSession(sessionId, config);
659
+ await endSessionWithRouting(ctx, {
660
+ sessionId,
661
+ config,
662
+ fallbackRegion: resolvedRegion,
663
+ });
253
664
  }
254
665
 
255
666
  return {
@@ -259,7 +670,11 @@ export const act = action({
259
670
  };
260
671
  } catch (error) {
261
672
  if (ownSession) {
262
- await api.endSession(sessionId, config);
673
+ await endSessionWithRouting(ctx, {
674
+ sessionId,
675
+ config,
676
+ fallbackRegion: resolvedRegion,
677
+ });
263
678
  }
264
679
  throw error;
265
680
  }
@@ -281,15 +696,18 @@ export const observe = action({
281
696
  url: v.optional(v.string()),
282
697
  instruction: v.string(),
283
698
  browserbaseSessionCreateParams: v.optional(v.any()),
699
+ model: modelValidator,
700
+ sessionConfig: sessionConfigValidator,
284
701
  options: v.optional(
285
702
  v.object({
286
703
  timeout: v.optional(v.number()),
287
704
  waitUntil: v.optional(waitUntilValidator),
705
+ selector: v.optional(v.string()),
288
706
  }),
289
707
  ),
290
708
  },
291
709
  returns: v.array(observedActionValidator),
292
- handler: async (_ctx: any, args: any) => {
710
+ handler: async (ctx: any, args: any) => {
293
711
  if (!args.sessionId && !args.url) {
294
712
  throw new Error("Either sessionId or url must be provided");
295
713
  }
@@ -303,26 +721,83 @@ export const observe = action({
303
721
 
304
722
  const ownSession = !args.sessionId;
305
723
  let sessionId = args.sessionId;
724
+ let resolvedRegion: api.BrowserbaseRegion | undefined;
306
725
 
307
726
  if (ownSession) {
727
+ resolvedRegion =
728
+ getRequestedRegion(args.browserbaseSessionCreateParams) ??
729
+ DEFAULT_BROWSERBASE_REGION;
308
730
  const session = await api.startSession(config, {
309
731
  browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
732
+ model: args.model,
733
+ ...args.sessionConfig,
310
734
  });
311
735
  sessionId = session.sessionId;
736
+ await persistSessionMetadata(ctx, {
737
+ sessionId,
738
+ region: resolvedRegion,
739
+ status: "active",
740
+ operation: "observe",
741
+ url: args.url,
742
+ });
743
+ }
744
+
745
+ if (!sessionId) {
746
+ throw new Error("Failed to initialize session");
747
+ }
748
+
749
+ if (!ownSession) {
750
+ resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
312
751
  }
313
752
 
314
753
  try {
315
754
  if (ownSession && args.url) {
316
- await api.navigate(sessionId, args.url, config, {
317
- waitUntil: args.options?.waitUntil,
318
- timeout: args.options?.timeout,
755
+ await runWithRegionRetry(ctx, {
756
+ sessionId,
757
+ initialRegion: resolvedRegion,
758
+ onRegionResolved: async (region) => {
759
+ resolvedRegion = region;
760
+ },
761
+ run: async (region) =>
762
+ api.navigate(
763
+ sessionId,
764
+ args.url,
765
+ config,
766
+ {
767
+ waitUntil: args.options?.waitUntil,
768
+ timeout: args.options?.timeout,
769
+ },
770
+ region,
771
+ ),
319
772
  });
320
773
  }
321
774
 
322
- const result = await api.observe(sessionId, args.instruction, config);
775
+ const result = await runWithRegionRetry(ctx, {
776
+ sessionId,
777
+ initialRegion: resolvedRegion,
778
+ onRegionResolved: async (region) => {
779
+ resolvedRegion = region;
780
+ },
781
+ run: async (region) =>
782
+ api.observe(
783
+ sessionId,
784
+ args.instruction,
785
+ config,
786
+ {
787
+ model: args.model,
788
+ timeout: args.options?.timeout,
789
+ selector: args.options?.selector,
790
+ },
791
+ region,
792
+ ),
793
+ });
323
794
 
324
795
  if (ownSession) {
325
- await api.endSession(sessionId, config);
796
+ await endSessionWithRouting(ctx, {
797
+ sessionId,
798
+ config,
799
+ fallbackRegion: resolvedRegion,
800
+ });
326
801
  }
327
802
 
328
803
  return result.result.map((action) => ({
@@ -330,10 +805,15 @@ export const observe = action({
330
805
  selector: action.selector,
331
806
  method: action.method,
332
807
  arguments: action.arguments,
808
+ backendNodeId: action.backendNodeId,
333
809
  }));
334
810
  } catch (error) {
335
811
  if (ownSession) {
336
- await api.endSession(sessionId, config);
812
+ await endSessionWithRouting(ctx, {
813
+ sessionId,
814
+ config,
815
+ fallbackRegion: resolvedRegion,
816
+ });
337
817
  }
338
818
  throw error;
339
819
  }
@@ -356,13 +836,20 @@ export const agent = action({
356
836
  url: v.optional(v.string()),
357
837
  instruction: v.string(),
358
838
  browserbaseSessionCreateParams: v.optional(v.any()),
839
+ model: modelValidator,
840
+ sessionConfig: sessionConfigValidator,
359
841
  options: v.optional(
360
842
  v.object({
361
843
  cua: v.optional(v.boolean()),
844
+ mode: v.optional(v.string()),
362
845
  maxSteps: v.optional(v.number()),
363
846
  systemPrompt: v.optional(v.string()),
364
847
  timeout: v.optional(v.number()),
365
848
  waitUntil: v.optional(waitUntilValidator),
849
+ executionModel: modelValidator,
850
+ provider: v.optional(v.string()),
851
+ highlightCursor: v.optional(v.boolean()),
852
+ shouldCache: v.optional(v.boolean()),
366
853
  }),
367
854
  ),
368
855
  },
@@ -371,8 +858,18 @@ export const agent = action({
371
858
  completed: v.boolean(),
372
859
  message: v.string(),
373
860
  success: v.boolean(),
861
+ metadata: v.optional(v.any()),
862
+ usage: v.optional(
863
+ v.object({
864
+ input_tokens: v.number(),
865
+ output_tokens: v.number(),
866
+ reasoning_tokens: v.optional(v.number()),
867
+ cached_input_tokens: v.optional(v.number()),
868
+ inference_time_ms: v.number(),
869
+ }),
870
+ ),
374
871
  }),
375
- handler: async (_ctx: any, args: any) => {
872
+ handler: async (ctx: any, args: any) => {
376
873
  if (!args.sessionId && !args.url) {
377
874
  throw new Error("Either sessionId or url must be provided");
378
875
  }
@@ -386,43 +883,128 @@ export const agent = action({
386
883
 
387
884
  const ownSession = !args.sessionId;
388
885
  let sessionId = args.sessionId;
886
+ let resolvedRegion: api.BrowserbaseRegion | undefined;
389
887
 
390
888
  if (ownSession) {
889
+ resolvedRegion =
890
+ getRequestedRegion(args.browserbaseSessionCreateParams) ??
891
+ DEFAULT_BROWSERBASE_REGION;
391
892
  const session = await api.startSession(config, {
392
893
  browserbaseSessionCreateParams: args.browserbaseSessionCreateParams,
894
+ model: args.model,
895
+ ...args.sessionConfig,
393
896
  });
394
897
  sessionId = session.sessionId;
898
+ await persistSessionMetadata(ctx, {
899
+ sessionId,
900
+ region: resolvedRegion,
901
+ status: "active",
902
+ operation: "workflow",
903
+ url: args.url,
904
+ });
905
+ }
906
+
907
+ if (!sessionId) {
908
+ throw new Error("Failed to initialize session");
909
+ }
910
+
911
+ if (!ownSession) {
912
+ resolvedRegion = await resolveSessionRegion(ctx, sessionId, resolvedRegion);
395
913
  }
396
914
 
397
915
  try {
398
916
  if (ownSession && args.url) {
399
- await api.navigate(sessionId, args.url, config, {
400
- waitUntil: args.options?.waitUntil,
401
- timeout: args.options?.timeout,
917
+ await runWithRegionRetry(ctx, {
918
+ sessionId,
919
+ initialRegion: resolvedRegion,
920
+ onRegionResolved: async (region) => {
921
+ resolvedRegion = region;
922
+ },
923
+ run: async (region) =>
924
+ api.navigate(
925
+ sessionId,
926
+ args.url,
927
+ config,
928
+ {
929
+ waitUntil: args.options?.waitUntil,
930
+ timeout: args.options?.timeout,
931
+ },
932
+ region,
933
+ ),
402
934
  });
403
935
  }
404
936
 
405
- const result = await api.agentExecute(
937
+ const result = await runWithRegionRetry(ctx, {
406
938
  sessionId,
407
- {
408
- cua: args.options?.cua,
409
- systemPrompt: args.options?.systemPrompt,
410
- },
411
- {
412
- instruction: args.instruction,
413
- maxSteps: args.options?.maxSteps,
939
+ initialRegion: resolvedRegion,
940
+ onRegionResolved: async (region) => {
941
+ resolvedRegion = region;
414
942
  },
415
- config,
416
- );
943
+ run: async (region) =>
944
+ api.agentExecute(
945
+ sessionId,
946
+ {
947
+ cua: args.options?.cua,
948
+ mode: args.options?.mode,
949
+ model: args.model,
950
+ systemPrompt: args.options?.systemPrompt,
951
+ executionModel: args.options?.executionModel,
952
+ provider: args.options?.provider,
953
+ },
954
+ {
955
+ instruction: args.instruction,
956
+ maxSteps: args.options?.maxSteps,
957
+ highlightCursor: args.options?.highlightCursor,
958
+ },
959
+ config,
960
+ args.options?.shouldCache,
961
+ region,
962
+ ),
963
+ });
417
964
 
418
965
  if (ownSession) {
419
- await api.endSession(sessionId, config);
966
+ await endSessionWithRouting(ctx, {
967
+ sessionId,
968
+ config,
969
+ fallbackRegion: resolvedRegion,
970
+ });
420
971
  }
421
972
 
422
- return result.result;
973
+ // Strip passthrough fields to match the Convex return validator.
974
+ // The API may return extra fields (e.g. timestamp, messages) not in the validator.
975
+ const r = result.result;
976
+ return {
977
+ actions: r.actions.map((a: any) => ({
978
+ type: a.type,
979
+ action: a.action,
980
+ reasoning: a.reasoning,
981
+ timeMs: a.timeMs,
982
+ taskCompleted: a.taskCompleted,
983
+ pageText: a.pageText,
984
+ pageUrl: a.pageUrl,
985
+ instruction: a.instruction,
986
+ })),
987
+ completed: r.completed,
988
+ message: r.message,
989
+ success: r.success,
990
+ metadata: r.metadata,
991
+ usage: r.usage
992
+ ? {
993
+ input_tokens: r.usage.input_tokens,
994
+ output_tokens: r.usage.output_tokens,
995
+ reasoning_tokens: r.usage.reasoning_tokens,
996
+ cached_input_tokens: r.usage.cached_input_tokens,
997
+ inference_time_ms: r.usage.inference_time_ms,
998
+ }
999
+ : undefined,
1000
+ };
423
1001
  } catch (error) {
424
1002
  if (ownSession) {
425
- await api.endSession(sessionId, config);
1003
+ await endSessionWithRouting(ctx, {
1004
+ sessionId,
1005
+ config,
1006
+ fallbackRegion: resolvedRegion,
1007
+ });
426
1008
  }
427
1009
  throw error;
428
1010
  }