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
6 changes: 6 additions & 0 deletions .changeset/shy-clouds-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@browserbasehq/stagehand-server": minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming -- is this minor b/c we're updating the API contract? If so, makes sense to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup!

"@browserbasehq/stagehand": minor
---

Using `mode` enum instead of old `cua` boolean in openapi spec
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export function validateExperimentalFeatures(
const { isExperimental, agentConfig, executeOptions, isStreaming } = options;

// Check if CUA mode is enabled (via mode: "cua" or deprecated cua: true)
const isCuaMode = agentConfig?.mode === "cua" || agentConfig?.cua === true;
const isCuaMode =
agentConfig?.mode !== undefined
? agentConfig.mode === "cua"
: agentConfig?.cua === true;

// CUA-specific validation: certain features are not available at all
if (isCuaMode) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/v3/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ export class StagehandAPIClient {

const wireAgentConfig: Api.AgentExecuteRequest["agentConfig"] = {
systemPrompt: agentConfig.systemPrompt,
cua: agentConfig.cua,
mode: agentConfig.mode ?? (agentConfig.cua === true ? "cua" : undefined),
cua: agentConfig.mode === undefined ? agentConfig.cua : undefined,
model: agentConfig.model
? (this.prepareModelConfig(
agentConfig.model as unknown as ModelConfiguration,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/lib/v3/cache/AgentCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ export class AgentCache {
);

const isCuaMode =
agentOptions?.mode === "cua" || agentOptions?.cua === true;
agentOptions?.mode !== undefined
? agentOptions.mode === "cua"
: agentOptions?.cua === true;

return JSON.stringify({
v3Model: this.getBaseModelName(),
Expand Down
8 changes: 7 additions & 1 deletion packages/core/lib/v3/types/public/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,15 @@ export const AgentConfigSchema = z
description: "Custom system prompt for the agent",
}),
cua: z.boolean().optional().meta({
description: "Enable Computer Use Agent mode",
description:
"Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence.",
example: true,
}),
mode: z.enum(["dom", "hybrid", "cua"]).optional().meta({
description:
"Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.",
example: "cua",
}),
})
.meta({ id: "AgentConfig" });

Expand Down
5 changes: 4 additions & 1 deletion packages/core/lib/v3/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1734,7 +1734,10 @@ export class V3 {
) => Promise<AgentResult | AgentStreamResult>;
} {
// Determine if CUA mode is enabled (via mode: "cua" or deprecated cua: true)
const isCuaMode = options?.mode === "cua" || options?.cua === true;
const isCuaMode =
options?.mode !== undefined
? options.mode === "cua"
: options?.cua === true;

// Emit deprecation warning for cua: true
if (options?.cua === true) {
Expand Down
24 changes: 22 additions & 2 deletions packages/server/openapi.v3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -664,9 +664,19 @@ components:
description: Custom system prompt for the agent
type: string
cua:
description: Enable Computer Use Agent mode
description:
"Deprecated. Use mode: 'cua' instead. If both are provided, mode
takes precedence."
example: true
type: boolean
mode:
description: Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.
example: cua
type: string
enum:
- dom
- hybrid
- cua
AgentAction:
type: object
properties:
Expand Down Expand Up @@ -1519,9 +1529,19 @@ components:
description: Custom system prompt for the agent
type: string
cua:
description: Enable Computer Use Agent mode
description:
"Deprecated. Use mode: 'cua' instead. If both are provided, mode
takes precedence."
example: true
type: boolean
mode:
description: Tool mode for the agent (dom, hybrid, cua). If set, overrides cua.
example: cua
type: string
enum:
- dom
- hybrid
- cua
additionalProperties: false
AgentUsageOutput:
type: object
Expand Down
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/node": "22.13.1",
"esbuild": "0.27.2",
"eslint": "^9.7.0",
"eslint-plugin-security": "^3.0.1",
"openai": "4.87.1",
"postject": "1.0.0-alpha.6",
"prettier": "^3.2.5",
Expand Down
72 changes: 72 additions & 0 deletions packages/server/test/integration/v3/agentExecute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,50 @@ describe("POST /v1/sessions/:id/agentExecute (V3) - CUA flag compatibility", ()
assertFetchOk(ctx.body !== null, "Response body should be parseable", ctx);
assertFetchOk(!ctx.body.success, "Response should indicate failure", ctx);
});

it("should prefer mode over cua when both are provided", async () => {
const url = getBaseUrl();
const openaiApiKey = requireEnv("OPENAI_API_KEY", OPENAI_API_KEY);

const ctx = await fetchWithContext<{
success: boolean;
data?: { result: unknown; actionId?: string };
}>(`${url}/v1/sessions/${sessionId}/agentExecute`, {
method: "POST",
headers,
body: JSON.stringify({
agentConfig: {
cua: true,
mode: "dom",
model: {
modelName: "openai/gpt-4.1-nano",
apiKey: openaiApiKey,
},
},
executeOptions: {
instruction: "What is the title of this page?",
},
}),
});

assertFetchStatus(
ctx,
HTTP_OK,
"V3 agent execute with mode: dom and cua: true should succeed",
);
assertFetchOk(ctx.body !== null, "Response body should be parseable", ctx);
assertFetchOk(ctx.body.success, "Response should indicate success", ctx);
assertFetchOk(
ctx.body.data !== undefined,
"Response should have data",
ctx,
);
assertFetchOk(
ctx.body.data.result !== undefined,
"Response should have result",
ctx,
);
});
});

// =============================================================================
Expand Down Expand Up @@ -989,4 +1033,32 @@ describe("POST /v1/sessions/:id/agentExecute - validation errors (V3)", () => {
ctx,
);
});

it("should return 400 for invalid agentConfig.mode", async () => {
const url = getBaseUrl();

const ctx = await fetchWithContext<{
success?: boolean;
error?: string;
message?: string;
}>(`${url}/v1/sessions/${sessionId}/agentExecute`, {
method: "POST",
headers,
body: JSON.stringify({
agentConfig: {
mode: "invalid-mode",
},
executeOptions: {
instruction: "Do something",
},
}),
});

assertFetchStatus(ctx, HTTP_BAD_REQUEST);
assertFetchOk(
!ctx.body?.success || ctx.body.error !== undefined,
"Response should indicate failure",
ctx,
);
});
});
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading