Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 50 additions & 29 deletions app/api/agent/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export const maxDuration = 600;

function sseEncode(event: string, data: unknown): Uint8Array {
const encoder = new TextEncoder();
return encoder.encode(`event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`);
return encoder.encode(
`event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`
);
}

function sseComment(comment: string): Uint8Array {
Expand All @@ -18,7 +20,11 @@ function sseComment(comment: string): Uint8Array {

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const [sessionId, goal] = [searchParams.get("sessionId"), searchParams.get("goal")];
const [sessionId, goal, fromChat] = [
searchParams.get("sessionId"),
searchParams.get("goal"),
searchParams.get("fromChat") === "true",
];

if (!sessionId || !goal) {
return new Response(
Expand All @@ -39,11 +45,14 @@ export async function GET(request: Request) {
}, 15000);

let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
timeoutTimer = setTimeout(async () => {
console.log(`[SSE] Timeout reached for session ${sessionId}`);
send("error", { message: "Agent run timed out after 10 minutes" });
await cleanup();
}, 10 * 60 * 1000);
timeoutTimer = setTimeout(
async () => {
console.log(`[SSE] Timeout reached for session ${sessionId}`);
send("error", { message: "Agent run timed out after 10 minutes" });
await cleanup();
},
10 * 60 * 1000
);

let closed = false;

Expand All @@ -52,7 +61,10 @@ export async function GET(request: Request) {
try {
controller.enqueue(chunk);
} catch (err) {
console.error(`[SSE] enqueue error`, err instanceof Error ? err.message : String(err));
console.error(
`[SSE] enqueue error`,
err instanceof Error ? err.message : String(err)
);
}
};

Expand All @@ -61,7 +73,10 @@ export async function GET(request: Request) {
try {
safeEnqueue(sseEncode(event, data));
} catch (err) {
console.error(`[SSE] send error`, err instanceof Error ? err.message : String(err));
console.error(
`[SSE] send error`,
err instanceof Error ? err.message : String(err)
);
}
};

Expand Down Expand Up @@ -89,19 +104,24 @@ export async function GET(request: Request) {
}, 15000);

// Hard timeout at 10 minutes
timeoutTimer = setTimeout(async () => {
console.log(`[SSE] Timeout reached for session ${sessionId}`);
send("error", { message: "Agent run timed out after 10 minutes" });
await cleanup();
}, 10 * 60 * 1000);
timeoutTimer = setTimeout(
async () => {
console.log(`[SSE] Timeout reached for session ${sessionId}`);
send("error", { message: "Agent run timed out after 10 minutes" });
await cleanup();
},
10 * 60 * 1000
);

console.log(`[SSE] Starting Stagehand agent run`, {
sessionId,
goal,
hasInstructions: true,
});

const logger = createStagehandUserLogger(send, { forwardStepEvents: false });
const logger = createStagehandUserLogger(send, {
forwardStepEvents: false,
});

const stagehand = new Stagehand({
env: "BROWSERBASE",
Expand All @@ -118,6 +138,7 @@ export async function GET(request: Request) {
width: 1288,
height: 711,
},
solveCaptchas: !fromChat, // false if session is from a search param, true otherwise
},
},
useAPI: false,
Expand All @@ -140,7 +161,7 @@ export async function GET(request: Request) {
});

const agent = stagehand.agent({
provider: "google",
provider: "google",
model: "computer-use-preview-10-2025",
options: {
apiKey: process.env.GOOGLE_API_KEY,
Expand All @@ -149,24 +170,24 @@ export async function GET(request: Request) {
});

const result = await agent.execute({
instruction: goal,
autoScreenshot: true,
waitBetweenActions: 200,
maxSteps: 100,
instruction: goal,
autoScreenshot: true,
waitBetweenActions: 200,
maxSteps: 100,
});

try {
console.log(`[SSE] metrics snapshot`, stagehand.metrics);
send("metrics", stagehand.metrics);
console.log(`[SSE] metrics snapshot`, stagehand.metrics);
send("metrics", stagehand.metrics);
} catch {}

const finalMessage = logger.getLastReasoning();
console.log(`[SSE] done`, {
success: result.success,
completed: result.completed,
finalMessage: finalMessage
});
send("done", { ...result, finalMessage });
const finalMessage = logger.getLastReasoning();
console.log(`[SSE] done`, {
success: result.success,
completed: result.completed,
finalMessage: finalMessage,
});
send("done", { ...result, finalMessage });

await cleanup(stagehand);
} catch (error) {
Expand Down
179 changes: 111 additions & 68 deletions app/api/session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,70 +7,107 @@ type BrowserbaseRegion =
| "eu-central-1"
| "ap-southeast-1";

// Exact timezone matches for east coast cities
const exactTimezoneMap: Record<string, BrowserbaseRegion> = {
"America/New_York": "us-east-1",
"America/Detroit": "us-east-1",
"America/Toronto": "us-east-1",
"America/Montreal": "us-east-1",
"America/Boston": "us-east-1",
"America/Chicago": "us-east-1",
// Timezone abbreviation to region mapping
const timezoneAbbreviationMap: Record<string, BrowserbaseRegion> = {
// US East Coast
EST: "us-east-1",
EDT: "us-east-1",

// US West Coast
PST: "us-west-2",
PDT: "us-west-2",

// US Mountain/Central - route to appropriate region
MST: "us-west-2",
MDT: "us-west-2",
CST: "us-east-1",
CDT: "us-east-1",

// Europe
GMT: "eu-central-1",
BST: "eu-central-1",
CET: "eu-central-1",
CEST: "eu-central-1",
EET: "eu-central-1",
EEST: "eu-central-1",
WET: "eu-central-1",
WEST: "eu-central-1",

// Asia-Pacific
JST: "ap-southeast-1", // Japan Standard Time
// Note: CST conflicts between US Central and China Standard - US takes priority
KST: "ap-southeast-1", // Korea Standard Time
IST: "ap-southeast-1",
AEST: "ap-southeast-1",
AEDT: "ap-southeast-1",
AWST: "ap-southeast-1",
NZST: "ap-southeast-1",
NZDT: "ap-southeast-1",
};

// Prefix-based region mapping
const prefixToRegion: Record<string, BrowserbaseRegion> = {
America: "us-west-2",
US: "us-west-2",
Canada: "us-west-2",
Europe: "eu-central-1",
Africa: "eu-central-1",
Asia: "ap-southeast-1",
Australia: "ap-southeast-1",
Pacific: "ap-southeast-1",
// Probability distributions for region routing
const distributions: Record<
BrowserbaseRegion,
Record<BrowserbaseRegion, number>
> = {
"us-west-2": {
"us-west-2": 100,
"us-east-1": 0,
"eu-central-1": 0,
"ap-southeast-1": 0,
},
"us-east-1": {
"us-east-1": 100,
"us-west-2": 0,
"eu-central-1": 0,
"ap-southeast-1": 0,
},
"eu-central-1": {
"eu-central-1": 100,
"us-east-1": 0,
"us-west-2": 0,
"ap-southeast-1": 0,
},
"ap-southeast-1": {
"ap-southeast-1": 100,
"eu-central-1": 0,
"us-west-2": 0,
"us-east-1": 0,
},
};

// Offset ranges to regions (inclusive bounds)
const offsetRanges: {
min: number;
max: number;
region: BrowserbaseRegion;
}[] = [
{ min: -24, max: -4, region: "us-west-2" }, // UTC-24 to UTC-4
{ min: -3, max: 4, region: "eu-central-1" }, // UTC-3 to UTC+4
{ min: 5, max: 24, region: "ap-southeast-1" }, // UTC+5 to UTC+24
];

function getClosestRegion(timezone?: string): BrowserbaseRegion {
try {
if (!timezone) {
return "us-west-2"; // Default if no timezone provided
function selectRegionWithProbability(
baseRegion: BrowserbaseRegion
): BrowserbaseRegion {
const distribution = distributions[baseRegion];
const random = Math.random() * 100; // Generate random number between 0-100

let cumulativeProbability = 0;
for (const [region, probability] of Object.entries(distribution)) {
cumulativeProbability += probability;
if (random < cumulativeProbability) {
return region as BrowserbaseRegion;
}
}

// Check exact matches first
if (timezone in exactTimezoneMap) {
return exactTimezoneMap[timezone];
}
// Fallback to base region if something goes wrong
return baseRegion;
}

// Check prefix matches
const prefix = timezone.split("/")[0];
if (prefix in prefixToRegion) {
return prefixToRegion[prefix];
function getRegionFromTimezoneAbbr(timezoneAbbr?: string): BrowserbaseRegion {
try {
if (!timezoneAbbr) {
return "us-west-2"; // Default if no timezone provided
}

// Use offset-based fallback
const date = new Date();
// Create a date formatter for the given timezone
const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone });
// Get the timezone offset in minutes
const timeString = formatter.format(date);
const testDate = new Date(timeString);
const hourOffset = (testDate.getTime() - date.getTime()) / (1000 * 60 * 60);

const matchingRange = offsetRanges.find(
(range) => hourOffset >= range.min && hourOffset <= range.max
);
// Direct lookup from timezone abbreviation
const region = timezoneAbbreviationMap[timezoneAbbr.toUpperCase()];
if (region) {
return region;
}

return matchingRange?.region ?? "us-west-2";
// Fallback to us-west-2 for unknown abbreviations
return "us-west-2";
} catch {
return "us-west-2";
}
Expand All @@ -81,26 +118,32 @@ async function createSession(timezone?: string) {
apiKey: process.env.BROWSERBASE_API_KEY!,
});

const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings = {
viewport: {
width: 2560,
height: 1440,
},
//@ts-expect-error - not present in the types, but valid
os: "windows",
blockAds: true,
advancedStealth: true
};

console.log("timezone ", timezone);
console.log("getClosestRegion(timezone)", getClosestRegion(timezone));
const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings =
{
viewport: {
width: 2560,
height: 1440,
},
// @ts-expect-error - os is not a valid property
os: "windows",
blockAds: true,
advancedStealth: true,
};

// Use timezone abbreviation to determine region
const closestRegion = getRegionFromTimezoneAbbr(timezone);
console.log("timezone abbreviation:", timezone);
console.log("mapped to region:", closestRegion);

const finalRegion = selectRegionWithProbability(closestRegion);
console.log("finalRegion after probability routing", finalRegion);

const session = await bb.sessions.create({
projectId: process.env.BROWSERBASE_PROJECT_ID!,
proxies: true,
browserSettings,
keepAlive: true,
region: getClosestRegion(timezone),
region: finalRegion,
});
return {
session,
Expand Down
4 changes: 3 additions & 1 deletion app/components/BrowserSessionContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface BrowserSessionContainerProps {
isCompleted: boolean;
initialMessage: string | undefined;
sessionTime?: number;
isFromSearchParam?: boolean;
onStop?: () => void;
onRestart?: () => void;
}
Expand Down Expand Up @@ -104,6 +105,7 @@ const BrowserSessionContainer: React.FC<BrowserSessionContainerProps> = ({
isCompleted,
initialMessage,
sessionTime = 0,
isFromSearchParam = false,
onStop = () => {},
onRestart = () => {},
}) => {
Expand Down Expand Up @@ -192,7 +194,7 @@ const BrowserSessionContainer: React.FC<BrowserSessionContainerProps> = ({
sessionUrl ? (
<iframe
src={sessionUrl}
className="w-full h-full border-none pointer-events-none"
className={`w-full h-full border-none ${!isFromSearchParam ? 'pointer-events-none' : ''}`}
sandbox="allow-same-origin allow-scripts allow-forms"
allow="clipboard-read; clipboard-write"
loading="lazy"
Expand Down
Loading