Skip to content

Commit 6a5496f

Browse files
authored
kill cua agent on stagehand.close (#1399)
# Why CUA agent loops continued running after `stagehand.close()` was called, causing errors like "Cannot read properties of null (reading 'awaitActivePage')". # What Changed - Added `StagehandClosedError` class to detect when Stagehand session is closed - `V3CuaAgentHandler` now checks if context is null before executing actions/screenshots and throws `StagehandClosedError` - All CUA clients (Anthropic, Google, OpenAI, Microsoft) re-throw `StagehandClosedError` in catch blocks to stop the agent loop immediately instead of swallowing the error <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Stops CUA agent loops immediately after stagehand.close() to prevent null-context errors and stray actions. Adds a clear signal so clients and handlers halt cleanly. - **Bug Fixes** - Introduced StagehandClosedError to indicate a closed session. - V3CuaAgentHandler checks context before screenshots/actions and throws StagehandClosedError. - Anthropic, Google, and OpenAI CUA clients rethrow StagehandClosedError to exit loops. <sup>Written for commit d34c5cd. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent 934f492 commit 6a5496f

File tree

7 files changed

+49
-2
lines changed

7 files changed

+49
-2
lines changed

.changeset/five-mammals-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Ensure cua agent is killed when stagehand.close is called

packages/core/lib/v3/agent/AnthropicCUAClient.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
} from "../types/public/agent";
1212
import { LogLine } from "../types/public/logs";
1313
import { ClientOptions } from "../types/public/model";
14-
import { AgentScreenshotProviderError } from "../types/public/sdkErrors";
14+
import {
15+
AgentScreenshotProviderError,
16+
StagehandClosedError,
17+
} from "../types/public/sdkErrors";
1518
import Anthropic from "@anthropic-ai/sdk";
1619
import { ToolSet } from "ai";
1720
import { AgentClient } from "./AgentClient";
@@ -320,6 +323,9 @@ export class AnthropicCUAClient extends AgentClient {
320323
});
321324
await this.actionHandler(action);
322325
} catch (error) {
326+
if (error instanceof StagehandClosedError) {
327+
throw error;
328+
}
323329
const errorMessage =
324330
error instanceof Error ? error.message : String(error);
325331
logger({

packages/core/lib/v3/agent/GoogleCUAClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { AgentClient } from "./AgentClient";
2020
import {
2121
AgentScreenshotProviderError,
2222
LLMResponseError,
23+
StagehandClosedError,
2324
} from "../types/public/sdkErrors";
2425
import { buildGoogleCUASystemPrompt } from "../../prompt";
2526
import { compressGoogleConversationImages } from "./utils/imageCompression";
@@ -486,6 +487,9 @@ export class GoogleCUAClient extends AgentClient {
486487
await new Promise((resolve) => setTimeout(resolve, delay));
487488
}
488489
} catch (actionError) {
490+
if (actionError instanceof StagehandClosedError) {
491+
throw actionError;
492+
}
489493
logger({
490494
category: "agent",
491495
message: `Error executing action ${action.type}: ${actionError}`,

packages/core/lib/v3/agent/OpenAICUAClient.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
} from "../types/public/agent";
1313
import { ClientOptions } from "../types/public/model";
1414
import { AgentClient } from "./AgentClient";
15-
import { AgentScreenshotProviderError } from "../types/public/sdkErrors";
15+
import {
16+
AgentScreenshotProviderError,
17+
StagehandClosedError,
18+
} from "../types/public/sdkErrors";
1619
import { ToolSet } from "ai";
1720
import {
1821
SessionFileLogger,
@@ -547,6 +550,9 @@ export class OpenAICUAClient extends AgentClient {
547550

548551
nextInputItems.push(outputItem);
549552
} catch (error) {
553+
if (error instanceof StagehandClosedError) {
554+
throw error;
555+
}
550556
const errorMessage =
551557
error instanceof Error ? error.message : String(error);
552558

@@ -613,6 +619,9 @@ export class OpenAICUAClient extends AgentClient {
613619

614620
nextInputItems.push(errorOutputItem);
615621
} catch (screenshotError) {
622+
if (screenshotError instanceof StagehandClosedError) {
623+
throw screenshotError;
624+
}
616625
// If we can't capture a screenshot, just send the error
617626
logger({
618627
category: "agent",
@@ -688,6 +697,9 @@ export class OpenAICUAClient extends AgentClient {
688697

689698
nextInputItems.push(outputItem);
690699
} catch (error) {
700+
if (error instanceof StagehandClosedError) {
701+
throw error;
702+
}
691703
const errorMessage =
692704
error instanceof Error ? error.message : String(error);
693705

packages/core/lib/v3/handlers/v3CuaAgentHandler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { LogLine } from "../types/public/logs";
1515
import { type Action, V3FunctionName } from "../types/public/methods";
1616
import { SessionFileLogger } from "../flowLogger";
17+
import { StagehandClosedError } from "../types/public/sdkErrors";
1718

1819
export class V3CuaAgentHandler {
1920
private v3: V3;
@@ -46,16 +47,28 @@ export class V3CuaAgentHandler {
4647
this.agent = client;
4748
}
4849

50+
/**
51+
* Ensures the V3 context is still available (not closed).
52+
* Throws StagehandClosedError if stagehand.close() was called.
53+
*/
54+
private ensureNotClosed(): void {
55+
if (!this.v3.context) {
56+
throw new StagehandClosedError();
57+
}
58+
}
59+
4960
private setupAgentClient(): void {
5061
// Provide screenshots to the agent client
5162
this.agentClient.setScreenshotProvider(async () => {
63+
this.ensureNotClosed();
5264
const page = await this.v3.context.awaitActivePage();
5365
const base64 = await page.screenshot({ fullPage: false });
5466
return base64.toString("base64"); // base64 png
5567
});
5668

5769
// Provide action executor
5870
this.agentClient.setActionHandler(async (action) => {
71+
this.ensureNotClosed();
5972
action.pageUrl = (await this.v3.context.awaitActivePage()).url();
6073

6174
const defaultDelay = 500;

packages/core/lib/v3/types/public/sdkErrors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,9 @@ export class AgentAbortError extends StagehandError {
364364
this.reason = reason || "aborted";
365365
}
366366
}
367+
368+
export class StagehandClosedError extends StagehandError {
369+
constructor() {
370+
super("Stagehand session was closed");
371+
}
372+
}

packages/core/tests/public-api/public-error-types.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const publicErrorTypes = {
2626
StagehandAPIError: Stagehand.StagehandAPIError,
2727
StagehandAPIUnauthorizedError: Stagehand.StagehandAPIUnauthorizedError,
2828
StagehandClickError: Stagehand.StagehandClickError,
29+
StagehandClosedError: Stagehand.StagehandClosedError,
2930
StagehandDefaultError: Stagehand.StagehandDefaultError,
3031
StagehandDomProcessError: Stagehand.StagehandDomProcessError,
3132
StagehandElementNotFoundError: Stagehand.StagehandElementNotFoundError,

0 commit comments

Comments
 (0)