@browserbasehq/orca 3.7.1-preview.0 → 3.7.2-gemini-cua

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.
Files changed (107) hide show
  1. package/dist/cjs/lib/v3/agent/GoogleCUAClient.d.ts +23 -0
  2. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js +363 -206
  3. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  4. package/dist/cjs/lib/v3/agent/tools/click.js +16 -5
  5. package/dist/cjs/lib/v3/agent/tools/click.js.map +1 -1
  6. package/dist/cjs/lib/v3/agent/tools/clickAndHold.js +1 -1
  7. package/dist/cjs/lib/v3/agent/tools/clickAndHold.js.map +1 -1
  8. package/dist/cjs/lib/v3/agent/tools/dragAndDrop.js +2 -2
  9. package/dist/cjs/lib/v3/agent/tools/dragAndDrop.js.map +1 -1
  10. package/dist/cjs/lib/v3/agent/tools/fillFormVision.js +18 -7
  11. package/dist/cjs/lib/v3/agent/tools/fillFormVision.js.map +1 -1
  12. package/dist/cjs/lib/v3/agent/tools/scroll.js +1 -1
  13. package/dist/cjs/lib/v3/agent/tools/scroll.js.map +1 -1
  14. package/dist/cjs/lib/v3/agent/tools/type.js +13 -4
  15. package/dist/cjs/lib/v3/agent/tools/type.js.map +1 -1
  16. package/dist/cjs/lib/v3/agent/utils/coordinateNormalization.d.ts +5 -2
  17. package/dist/cjs/lib/v3/agent/utils/coordinateNormalization.js +5 -7
  18. package/dist/cjs/lib/v3/agent/utils/coordinateNormalization.js.map +1 -1
  19. package/dist/cjs/lib/v3/api.d.ts +1 -1
  20. package/dist/cjs/lib/v3/api.js +2 -1
  21. package/dist/cjs/lib/v3/api.js.map +1 -1
  22. package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js +20 -0
  23. package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
  24. package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.d.ts +4 -0
  25. package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js +126 -20
  26. package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
  27. package/dist/cjs/lib/v3/launch/local.js +1 -1
  28. package/dist/cjs/lib/v3/launch/local.js.map +1 -1
  29. package/dist/cjs/lib/v3/llm/aisdk.js +3 -3
  30. package/dist/cjs/lib/v3/llm/aisdk.js.map +1 -1
  31. package/dist/cjs/lib/v3/runtimePaths.js +0 -1
  32. package/dist/cjs/lib/v3/runtimePaths.js.map +1 -1
  33. package/dist/cjs/lib/v3/types/public/api.d.ts +53 -0
  34. package/dist/cjs/lib/v3/types/public/api.js +8 -0
  35. package/dist/cjs/lib/v3/types/public/api.js.map +1 -1
  36. package/dist/cjs/lib/v3/types/public/model.d.ts +5 -0
  37. package/dist/cjs/lib/v3/types/public/model.js.map +1 -1
  38. package/dist/cjs/lib/v3/types/public/options.d.ts +13 -0
  39. package/dist/cjs/lib/v3/types/public/options.js.map +1 -1
  40. package/dist/cjs/lib/v3/understudy/a11y/snapshot/coordinateResolver.js +39 -1
  41. package/dist/cjs/lib/v3/understudy/a11y/snapshot/coordinateResolver.js.map +1 -1
  42. package/dist/cjs/lib/v3/understudy/locator.d.ts +12 -0
  43. package/dist/cjs/lib/v3/understudy/locator.js +44 -0
  44. package/dist/cjs/lib/v3/understudy/locator.js.map +1 -1
  45. package/dist/cjs/lib/v3/understudy/page.d.ts +22 -3
  46. package/dist/cjs/lib/v3/understudy/page.js +55 -1
  47. package/dist/cjs/lib/v3/understudy/page.js.map +1 -1
  48. package/dist/cjs/lib/v3/v3.d.ts +14 -0
  49. package/dist/cjs/lib/v3/v3.js +42 -13
  50. package/dist/cjs/lib/v3/v3.js.map +1 -1
  51. package/dist/cjs/lib/version.d.ts +1 -1
  52. package/dist/cjs/lib/version.js +1 -1
  53. package/dist/cjs/lib/version.js.map +1 -1
  54. package/dist/esm/lib/v3/agent/GoogleCUAClient.d.ts +23 -0
  55. package/dist/esm/lib/v3/agent/GoogleCUAClient.js +363 -206
  56. package/dist/esm/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  57. package/dist/esm/lib/v3/agent/tools/click.js +16 -5
  58. package/dist/esm/lib/v3/agent/tools/click.js.map +1 -1
  59. package/dist/esm/lib/v3/agent/tools/clickAndHold.js +1 -1
  60. package/dist/esm/lib/v3/agent/tools/clickAndHold.js.map +1 -1
  61. package/dist/esm/lib/v3/agent/tools/dragAndDrop.js +2 -2
  62. package/dist/esm/lib/v3/agent/tools/dragAndDrop.js.map +1 -1
  63. package/dist/esm/lib/v3/agent/tools/fillFormVision.js +19 -8
  64. package/dist/esm/lib/v3/agent/tools/fillFormVision.js.map +1 -1
  65. package/dist/esm/lib/v3/agent/tools/scroll.js +1 -1
  66. package/dist/esm/lib/v3/agent/tools/scroll.js.map +1 -1
  67. package/dist/esm/lib/v3/agent/tools/type.js +13 -4
  68. package/dist/esm/lib/v3/agent/tools/type.js.map +1 -1
  69. package/dist/esm/lib/v3/agent/utils/coordinateNormalization.d.ts +5 -2
  70. package/dist/esm/lib/v3/agent/utils/coordinateNormalization.js +5 -7
  71. package/dist/esm/lib/v3/agent/utils/coordinateNormalization.js.map +1 -1
  72. package/dist/esm/lib/v3/api.d.ts +1 -1
  73. package/dist/esm/lib/v3/api.js +2 -1
  74. package/dist/esm/lib/v3/api.js.map +1 -1
  75. package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js +20 -0
  76. package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
  77. package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.d.ts +4 -0
  78. package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js +126 -20
  79. package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
  80. package/dist/esm/lib/v3/launch/local.js +1 -1
  81. package/dist/esm/lib/v3/launch/local.js.map +1 -1
  82. package/dist/esm/lib/v3/llm/aisdk.js +3 -3
  83. package/dist/esm/lib/v3/llm/aisdk.js.map +1 -1
  84. package/dist/esm/lib/v3/runtimePaths.js +0 -1
  85. package/dist/esm/lib/v3/runtimePaths.js.map +1 -1
  86. package/dist/esm/lib/v3/types/public/api.d.ts +53 -0
  87. package/dist/esm/lib/v3/types/public/api.js +8 -0
  88. package/dist/esm/lib/v3/types/public/api.js.map +1 -1
  89. package/dist/esm/lib/v3/types/public/model.d.ts +5 -0
  90. package/dist/esm/lib/v3/types/public/model.js.map +1 -1
  91. package/dist/esm/lib/v3/types/public/options.d.ts +13 -0
  92. package/dist/esm/lib/v3/types/public/options.js.map +1 -1
  93. package/dist/esm/lib/v3/understudy/a11y/snapshot/coordinateResolver.js +39 -1
  94. package/dist/esm/lib/v3/understudy/a11y/snapshot/coordinateResolver.js.map +1 -1
  95. package/dist/esm/lib/v3/understudy/locator.d.ts +12 -0
  96. package/dist/esm/lib/v3/understudy/locator.js +44 -0
  97. package/dist/esm/lib/v3/understudy/locator.js.map +1 -1
  98. package/dist/esm/lib/v3/understudy/page.d.ts +22 -3
  99. package/dist/esm/lib/v3/understudy/page.js +55 -1
  100. package/dist/esm/lib/v3/understudy/page.js.map +1 -1
  101. package/dist/esm/lib/v3/v3.d.ts +14 -0
  102. package/dist/esm/lib/v3/v3.js +42 -13
  103. package/dist/esm/lib/v3/v3.js.map +1 -1
  104. package/dist/esm/lib/version.d.ts +1 -1
  105. package/dist/esm/lib/version.js +1 -1
  106. package/dist/esm/lib/version.js.map +1 -1
  107. package/package.json +3 -3
@@ -10,6 +10,15 @@ const cuaKeyMapping_js_1 = require("./utils/cuaKeyMapping.js");
10
10
  const googleCustomToolHandler_js_1 = require("./utils/googleCustomToolHandler.js");
11
11
  const FlowLogger_js_1 = require("../flowlogger/FlowLogger.js");
12
12
  const uuid_1 = require("uuid");
13
+ /** Errors worth retrying: rate limits, server errors, network failures. */
14
+ function isTransientError(error) {
15
+ const status = error?.status;
16
+ if (typeof status === "number") {
17
+ return status === 429 || status >= 500;
18
+ }
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return /network|timeout|timed out|fetch failed|econn|socket hang up/i.test(message);
21
+ }
13
22
  /**
14
23
  * Client for Google's Computer Use Assistant API
15
24
  * This implementation uses the Google Generative AI SDK for Computer Use
@@ -27,6 +36,13 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
27
36
  tools;
28
37
  baseURL;
29
38
  safetyConfirmationHandler;
39
+ /**
40
+ * Request API. "interactions" uses the stateful Interactions API (server-side
41
+ * history via previous_interaction_id) instead of generateContent.
42
+ */
43
+ api = "generateContent";
44
+ previousInteractionId;
45
+ pendingInteractionInput;
30
46
  constructor(type, modelName, userProvidedInstructions, clientOptions, tools) {
31
47
  super(type, modelName, userProvidedInstructions);
32
48
  this.tools = tools;
@@ -44,19 +60,23 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
44
60
  ...(this.baseURL ? { httpOptions: { baseUrl: this.baseURL } } : {}),
45
61
  };
46
62
  this.client = new genai_1.GoogleGenAI(genAIOptions);
63
+ if (clientOptions?.googleApi === "interactions") {
64
+ this.api = "interactions";
65
+ }
47
66
  // Get environment if specified
48
67
  if (clientOptions?.environment &&
49
68
  typeof clientOptions.environment === "string") {
50
69
  this.environment = clientOptions.environment;
51
70
  }
71
+ // No maxOutputTokens: thinking tokens count against it on gemini-3.x, so a
72
+ // fixed cap truncates turns (finishReason MAX_TOKENS). Use the model max.
52
73
  this.generateContentConfig = {
53
74
  temperature: 1,
54
75
  topP: 0.95,
55
76
  topK: 40,
56
- maxOutputTokens: 8192,
57
- // systemInstruction: this.userProvidedInstructions
58
- // ? { parts: [{ text: this.userProvidedInstructions }] }
59
- // : { parts: [{ text: buildGoogleCUASystemPrompt() }] },
77
+ systemInstruction: this.userProvidedInstructions
78
+ ? this.userProvidedInstructions
79
+ : (0, prompt_js_1.buildGoogleCUASystemPrompt)().content,
60
80
  tools: [
61
81
  {
62
82
  computerUse: {
@@ -166,6 +186,7 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
166
186
  const maxSteps = options.maxSteps || 10;
167
187
  let currentStep = 0;
168
188
  let completed = false;
189
+ let declined = false;
169
190
  const actions = [];
170
191
  const messageList = [];
171
192
  let finalMessage = "";
@@ -196,6 +217,9 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
196
217
  actions.push(...result.actions);
197
218
  // Update completion status
198
219
  completed = result.completed;
220
+ if (result.stopped) {
221
+ declined = true;
222
+ }
199
223
  // Record any message for this step
200
224
  if (result.message) {
201
225
  messageList.push(result.message);
@@ -206,7 +230,7 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
206
230
  }
207
231
  // Return the final result
208
232
  return {
209
- success: completed,
233
+ success: completed && !declined,
210
234
  actions,
211
235
  message: finalMessage,
212
236
  completed,
@@ -246,20 +270,12 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
246
270
  */
247
271
  async initializeHistory(instruction) {
248
272
  const parts = [{ text: instruction }];
273
+ this.previousInteractionId = undefined;
274
+ this.pendingInteractionInput = instruction;
249
275
  // Note: The Python implementation doesn't include the initial screenshot
250
- // Following the same pattern here
251
- const systemPromptContent = this.userProvidedInstructions
252
- ? this.userProvidedInstructions
253
- : (0, prompt_js_1.buildGoogleCUASystemPrompt)().content;
276
+ // Following the same pattern here. The system prompt travels in
277
+ // generateContentConfig.systemInstruction.
254
278
  this.history = [
255
- {
256
- role: "user",
257
- parts: [
258
- {
259
- text: "System prompt: " + systemPromptContent,
260
- },
261
- ],
262
- },
263
279
  {
264
280
  role: "user",
265
281
  parts,
@@ -275,11 +291,14 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
275
291
  // Compress images in conversation history before sending to the model
276
292
  const compressedResult = (0, imageCompression_js_1.compressGoogleConversationImages)(this.history, 2);
277
293
  const compressedHistory = compressedResult.items;
278
- // Use the SDK's generateContent method with retry logic (matching Python's get_model_response)
294
+ // Use the SDK's generateContent method, retrying only transient failures
295
+ // (rate limits, 5xx, network, empty candidates). Anything else — a 400,
296
+ // or a candidate that stopped for a non-STOP reason — fails fast.
279
297
  const maxRetries = 5;
280
298
  const baseDelayS = 1;
281
299
  let lastError = null;
282
300
  let response = null;
301
+ let emptyResponse = false;
283
302
  // Log LLM request
284
303
  const llmRequestId = (0, uuid_1.v7)();
285
304
  FlowLogger_js_1.FlowLogger.logLlmRequest({
@@ -300,21 +319,20 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
300
319
  await new Promise((resolve) => setTimeout(resolve, delay));
301
320
  }
302
321
  // Use the SDK's generateContent method - following Python SDK pattern
303
- response = await this.client.models.generateContent({
304
- model: this.modelName,
305
- contents: compressedHistory,
306
- config: this.generateContentConfig,
307
- });
308
- // Check if we have valid response content
322
+ response = await this.requestModel(compressedHistory);
323
+ // An empty response is treated as transient and retried; an early
324
+ // stop (non-STOP finish reason) is not — it is rejected below.
309
325
  if (!response.candidates || response.candidates.length === 0) {
326
+ emptyResponse = true;
310
327
  throw new sdkErrors_js_1.LLMResponseError("agent", "Response has no candidates!");
311
328
  }
312
- const candidate = response.candidates[0];
313
- if (!candidate.content || !candidate.content.parts) {
314
- const reason = candidate.finishReason || "unknown";
315
- throw new sdkErrors_js_1.LLMResponseError("agent", `Response has no content (finish reason: ${reason})`);
329
+ const first = response.candidates[0];
330
+ const stoppedEarly = first.finishReason && first.finishReason !== "STOP";
331
+ if (!stoppedEarly && !first.content?.parts?.length) {
332
+ emptyResponse = true;
333
+ throw new sdkErrors_js_1.LLMResponseError("agent", `Response has no content (finish reason: ${first.finishReason || "unknown"})`);
316
334
  }
317
- // Success - we have a valid response
335
+ // Success - validated below, outside the retry loop
318
336
  break;
319
337
  }
320
338
  catch (error) {
@@ -324,6 +342,11 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
324
342
  message: `API call error: ${lastError.message}`,
325
343
  level: 2,
326
344
  });
345
+ const retryable = emptyResponse || isTransientError(error);
346
+ emptyResponse = false;
347
+ if (!retryable) {
348
+ throw lastError;
349
+ }
327
350
  // If this was the last attempt, throw the error
328
351
  if (attempt === maxRetries - 1) {
329
352
  logger({
@@ -338,6 +361,13 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
338
361
  if (!response) {
339
362
  throw (lastError || new Error("Failed to get response after all retries"));
340
363
  }
364
+ // A candidate that stopped for any reason other than STOP (MAX_TOKENS,
365
+ // SAFETY, MALFORMED_FUNCTION_CALL, ...) is incomplete: fail the run
366
+ // rather than executing its calls or reporting its text as the answer.
367
+ const candidate = response.candidates[0];
368
+ if (candidate.finishReason && candidate.finishReason !== "STOP") {
369
+ throw new sdkErrors_js_1.LLMResponseError("agent", `Model response stopped early (finish reason: ${candidate.finishReason})`);
370
+ }
341
371
  const endTime = Date.now();
342
372
  const elapsedMs = endTime - startTime;
343
373
  const { usageMetadata } = response;
@@ -371,63 +401,58 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
371
401
  }
372
402
  this.history.push(sanitizedContent);
373
403
  }
374
- // Execute actions and collect function responses
404
+ // Execute each function call and answer it with its own function
405
+ // response: the call's outcome (output or error text), the URL, and a
406
+ // fresh screenshot taken after that call. The model expects exactly one
407
+ // response per call, including calls with nothing to execute (e.g. a
408
+ // standalone take_screenshot) and calls that could not be converted.
375
409
  const functionResponses = [];
376
- // Always process the model's response, even when it produced no
377
- // executable action (e.g. a standalone take_screenshot). The model
378
- // expects exactly one function response — a fresh screenshot — per
379
- // computer-use call; skipping it when there are 0 actions leaves the
380
- // model blind and it gives up after one empty turn.
381
- {
382
- let hasError = false;
383
- // Execute all actions (a no-op when there are none).
384
- for (let i = 0; i < result.actions.length; i++) {
385
- const action = result.actions[i];
386
- logger({
387
- category: "agent",
388
- message: `Executing action ${i + 1}/${result.actions.length}: ${action.type}`,
389
- level: 2,
390
- });
391
- // Special handling for open_web_browser - don't execute it
392
- if (action.type === "open_web_browser") {
393
- // Set pageUrl for open_web_browser since it doesn't go through action handler
394
- action.pageUrl = this.currentUrl;
395
- logger({
396
- category: "agent",
397
- message: "Skipping open_web_browser action",
398
- level: 2,
399
- });
410
+ let stopped;
411
+ for (const call of result.calls) {
412
+ const { functionCall } = call;
413
+ if (call.actions[0]?.type === "custom_tool") {
414
+ const action = call.actions[0];
415
+ const toolName = action.name;
416
+ if (this.tools && toolName in this.tools) {
417
+ const executionResult = await (0, googleCustomToolHandler_js_1.executeGoogleCustomTool)(toolName, action.arguments, this.tools, functionCall, logger);
418
+ functionResponses.push(executionResult.functionResponse);
419
+ }
420
+ continue;
421
+ }
422
+ // Ask for confirmation BEFORE acting. If the user declines, the
423
+ // action is not executed and the run stops.
424
+ let safetyAcknowledgement;
425
+ if (functionCall.args?.safety_decision) {
426
+ safetyAcknowledgement = await this.handleSafetyConfirmation(functionCall.args.safety_decision, logger);
427
+ if (safetyAcknowledgement === undefined) {
428
+ stopped = `Stopped: the user declined to confirm "${functionCall.name}".`;
429
+ break;
400
430
  }
401
- else if (action.type === "screenshot") {
402
- // No interaction to perform — the screenshot captured below is
403
- // returned to the model as this call's function response.
431
+ }
432
+ let error = call.error;
433
+ if (!error) {
434
+ for (let i = 0; i < call.actions.length; i++) {
435
+ const action = call.actions[i];
404
436
  logger({
405
437
  category: "agent",
406
- message: "take_screenshot: capturing current page",
438
+ message: `Executing action ${i + 1}/${call.actions.length} for ${functionCall.name}: ${action.type}`,
407
439
  level: 2,
408
440
  });
409
- }
410
- else if (action.type === "custom_tool") {
411
- const toolName = action.name;
412
- const toolArgs = action.arguments;
413
- if (this.tools && toolName in this.tools) {
414
- const correspondingFunctionCall = result.functionCalls.find((fc) => fc.name === toolName);
415
- if (correspondingFunctionCall) {
416
- const executionResult = await (0, googleCustomToolHandler_js_1.executeGoogleCustomTool)(toolName, toolArgs, this.tools, correspondingFunctionCall, logger);
417
- functionResponses.push(executionResult.functionResponse);
418
- if (!executionResult.success) {
419
- hasError = true;
420
- }
421
- }
441
+ // open_web_browser and take_screenshot have nothing to execute;
442
+ // the screenshot captured below is the call's result.
443
+ if (action.type === "open_web_browser") {
444
+ action.pageUrl = this.currentUrl;
445
+ continue;
446
+ }
447
+ if (action.type === "screenshot" || !this.actionHandler) {
448
+ continue;
422
449
  }
423
- }
424
- else if (this.actionHandler) {
425
450
  try {
426
451
  await this.actionHandler(action);
427
452
  // Add a delay between actions to ensure they complete properly
428
453
  // Longer delay for typing actions to ensure fields are ready
429
- if (i < result.actions.length - 1) {
430
- const nextAction = result.actions[i + 1];
454
+ if (i < call.actions.length - 1) {
455
+ const nextAction = call.actions[i + 1];
431
456
  const isTypingAction = action.type === "type" || nextAction.type === "type";
432
457
  const delay = isTypingAction ? 500 : 200;
433
458
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -437,87 +462,88 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
437
462
  if (actionError instanceof sdkErrors_js_1.StagehandClosedError) {
438
463
  throw actionError;
439
464
  }
465
+ error =
466
+ actionError instanceof Error
467
+ ? actionError.message
468
+ : String(actionError);
440
469
  logger({
441
470
  category: "agent",
442
- message: `Error executing action ${action.type}: ${actionError}`,
471
+ message: `Error executing action ${action.type}: ${error}`,
443
472
  level: 0,
444
473
  });
445
- hasError = true;
446
- // Continue processing other actions even if one fails
474
+ // Skip the rest of this call; report the failure to the model.
475
+ break;
447
476
  }
448
477
  }
449
478
  }
450
- // Create function responses for computer use actions (non-custom tools)
451
- // We need exactly one response per function call, regardless of how many actions were generated
452
- if (result.functionCalls.length > 0 || hasError) {
453
- // Filter out custom tool function calls as they've already been handled
454
- const computerUseFunctionCalls = result.functionCalls.filter((fc) => !(0, googleCustomToolHandler_js_1.isCustomTool)(fc, this.tools));
455
- if (computerUseFunctionCalls.length > 0) {
456
- try {
457
- logger({
458
- category: "agent",
459
- message: `Taking screenshot after executing ${result.actions.length} actions${hasError ? " (with errors)" : ""}`,
460
- level: 2,
461
- });
462
- const screenshot = await this.captureScreenshot();
463
- const base64Data = screenshot.base64;
464
- // Create one function response for each computer use function call
465
- // Following Python SDK pattern: FunctionResponse with parts containing inline_data
466
- for (const functionCall of computerUseFunctionCalls) {
467
- let safetyAcknowledgement;
468
- if (functionCall.args?.safety_decision) {
469
- safetyAcknowledgement = await this.handleSafetyConfirmation(functionCall.args.safety_decision, logger);
470
- }
471
- const functionResponsePart = {
472
- functionResponse: {
473
- name: functionCall.name,
474
- response: {
475
- url: this.currentUrl || "",
476
- ...(safetyAcknowledgement !== undefined
477
- ? {
478
- safety_acknowledgement: safetyAcknowledgement,
479
- }
480
- : {}),
481
- },
482
- parts: [
483
- {
484
- inlineData: {
485
- mimeType: screenshot.mediaType,
486
- data: base64Data,
487
- },
488
- },
489
- ],
490
- },
491
- };
492
- functionResponses.push(functionResponsePart);
493
- }
494
- }
495
- catch (error) {
496
- logger({
497
- category: "agent",
498
- message: `Error capturing screenshot: ${error}`,
499
- level: 0,
500
- });
501
- }
502
- }
479
+ let screenshot;
480
+ let screenshotError;
481
+ try {
482
+ screenshot = await this.captureScreenshot();
503
483
  }
504
- // Add all function responses to history in a single user message
505
- if (functionResponses.length > 0) {
484
+ catch (screenshotFailure) {
485
+ if (screenshotFailure instanceof sdkErrors_js_1.StagehandClosedError) {
486
+ throw screenshotFailure;
487
+ }
488
+ screenshotError =
489
+ screenshotFailure instanceof Error
490
+ ? screenshotFailure.message
491
+ : String(screenshotFailure);
506
492
  logger({
507
493
  category: "agent",
508
- message: `Adding ${functionResponses.length} function responses to history`,
509
- level: 2,
510
- });
511
- this.history.push({
512
- role: "user",
513
- parts: functionResponses,
494
+ message: `Error capturing screenshot: ${screenshotError}`,
495
+ level: 0,
514
496
  });
515
497
  }
498
+ functionResponses.push({
499
+ functionResponse: {
500
+ ...(functionCall.id ? { id: functionCall.id } : {}),
501
+ name: functionCall.name,
502
+ response: {
503
+ url: this.currentUrl || "",
504
+ ...(error ? { error } : {}),
505
+ ...(screenshotError ? { screenshot_error: screenshotError } : {}),
506
+ ...(safetyAcknowledgement !== undefined
507
+ ? { safety_acknowledgement: safetyAcknowledgement }
508
+ : {}),
509
+ },
510
+ ...(screenshot
511
+ ? {
512
+ parts: [
513
+ {
514
+ inlineData: {
515
+ mimeType: screenshot.mediaType,
516
+ data: screenshot.base64,
517
+ },
518
+ },
519
+ ],
520
+ }
521
+ : {}),
522
+ },
523
+ });
524
+ }
525
+ // The Interactions API keeps history server-side: the next request sends
526
+ // only these results, chained by previous_interaction_id.
527
+ if (this.api === "interactions" && !stopped) {
528
+ this.pendingInteractionInput = functionResponses.map(GoogleCUAClient.toFunctionResult);
529
+ }
530
+ // Add all function responses to history in a single user message
531
+ if (!stopped && functionResponses.length > 0) {
532
+ logger({
533
+ category: "agent",
534
+ message: `Adding ${functionResponses.length} function responses to history`,
535
+ level: 2,
536
+ });
537
+ this.history.push({
538
+ role: "user",
539
+ parts: functionResponses,
540
+ });
516
541
  }
517
542
  return {
518
543
  actions: result.actions,
519
- message: result.message,
520
- completed: result.completed,
544
+ message: stopped ?? result.message,
545
+ completed: stopped ? true : result.completed,
546
+ stopped: stopped !== undefined,
521
547
  usage: {
522
548
  // promptTokenCount is the TOTAL input and already includes the
523
549
  // cached portion; cachedContentTokenCount is the cache-hit subset
@@ -541,19 +567,128 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
541
567
  throw error;
542
568
  }
543
569
  }
570
+ /** Send one model request on the configured API. */
571
+ async requestModel(contents) {
572
+ if (this.api === "interactions") {
573
+ return this.createInteraction();
574
+ }
575
+ return this.client.models.generateContent({
576
+ model: this.modelName,
577
+ contents,
578
+ config: this.generateContentConfig,
579
+ });
580
+ }
581
+ /**
582
+ * Create an interaction (Interactions API, REST) continuing the previous one,
583
+ * and map it onto the generateContent response shape the step loop expects.
584
+ */
585
+ async createInteraction() {
586
+ const baseUrl = (this.baseURL ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
587
+ const body = {
588
+ model: this.modelName.replace(/^models\//, ""),
589
+ input: this.pendingInteractionInput,
590
+ system_instruction: this.generateContentConfig.systemInstruction,
591
+ tools: [
592
+ {
593
+ type: "computer_use",
594
+ environment: this.environment === "ENVIRONMENT_DESKTOP" ? "desktop" : "browser",
595
+ },
596
+ ],
597
+ ...(this.previousInteractionId
598
+ ? { previous_interaction_id: this.previousInteractionId }
599
+ : {}),
600
+ };
601
+ const res = await fetch(`${baseUrl}/v1beta/interactions`, {
602
+ method: "POST",
603
+ headers: {
604
+ "content-type": "application/json",
605
+ "x-goog-api-key": this.apiKey,
606
+ },
607
+ body: JSON.stringify(body),
608
+ });
609
+ const json = (await res.json().catch(() => ({})));
610
+ if (!res.ok || !json.id) {
611
+ const { message = res.statusText, code } = json.error ?? {};
612
+ // code "safety": Google's input classifier rejected the turn. Not
613
+ // transient (status 400), so it fails the run instead of retrying.
614
+ throw Object.assign(new Error(code === "safety"
615
+ ? `Gemini blocked the request (safety): ${message}`
616
+ : `Interactions API error ${res.status}: ${message}`), { status: res.status });
617
+ }
618
+ const parts = [];
619
+ for (const step of json.steps ?? []) {
620
+ if (step.type === "function_call" && step.name) {
621
+ parts.push({
622
+ functionCall: { id: step.id, name: step.name, args: step.arguments },
623
+ });
624
+ }
625
+ else if (step.type === "model_output") {
626
+ for (const content of step.content ?? []) {
627
+ if (content.type === "text" && content.text) {
628
+ parts.push({ text: content.text });
629
+ }
630
+ }
631
+ }
632
+ }
633
+ if (parts.length === 0 && json.output_text) {
634
+ parts.push({ text: json.output_text });
635
+ }
636
+ this.previousInteractionId = json.id;
637
+ // requires_action (calls to answer) and completed are normal stops; any
638
+ // other status (incomplete, failed, budget_exceeded, ...) is an early stop.
639
+ const finishReason = json.status === "requires_action" || json.status === "completed"
640
+ ? "STOP"
641
+ : String(json.status).toUpperCase();
642
+ return {
643
+ candidates: [{ content: { role: "model", parts }, finishReason }],
644
+ usageMetadata: {
645
+ promptTokenCount: json.usage?.total_input_tokens,
646
+ candidatesTokenCount: json.usage?.total_output_tokens,
647
+ thoughtsTokenCount: json.usage?.total_thought_tokens,
648
+ cachedContentTokenCount: json.usage?.total_cached_tokens,
649
+ },
650
+ };
651
+ }
652
+ /** Convert a generateContent function response into an Interactions result. */
653
+ static toFunctionResult(part) {
654
+ const fr = part.functionResponse;
655
+ const response = { ...(fr.response ?? {}) };
656
+ // The Interactions API takes a boolean acknowledgement.
657
+ if (response.safety_acknowledgement !== undefined) {
658
+ response.safety_acknowledgement = true;
659
+ }
660
+ const image = fr.parts?.[0]?.inlineData;
661
+ return {
662
+ type: "function_result",
663
+ call_id: fr.id ?? "",
664
+ name: fr.name,
665
+ ...(response.error ? { is_error: true } : {}),
666
+ result: [
667
+ { type: "text", text: JSON.stringify(response) },
668
+ ...(image?.data
669
+ ? [
670
+ {
671
+ type: "image",
672
+ data: image.data,
673
+ mime_type: image.mimeType,
674
+ },
675
+ ]
676
+ : []),
677
+ ],
678
+ };
679
+ }
544
680
  /**
545
681
  * Process the response from Google's API
546
682
  */
547
683
  async processResponse(response, logger) {
548
- const actions = [];
684
+ const calls = [];
549
685
  let message = "";
550
- const functionCalls = [];
551
686
  if (!response.candidates || response.candidates.length === 0) {
552
687
  return {
553
688
  actions: [],
554
689
  message: "No candidates in response",
555
690
  completed: true,
556
- functionCalls: [],
691
+ calls: [],
557
692
  };
558
693
  }
559
694
  const candidate = response.candidates[0];
@@ -574,83 +709,71 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
574
709
  });
575
710
  }
576
711
  if (part.functionCall) {
577
- functionCalls.push(part.functionCall);
712
+ const functionCall = part.functionCall;
578
713
  logger({
579
714
  category: "agent",
580
- message: `Found function call: ${part.functionCall.name} with args: ${JSON.stringify(part.functionCall.args)}`,
715
+ message: `Found function call: ${functionCall.name} with args: ${JSON.stringify(functionCall.args)}`,
581
716
  level: 2,
582
717
  });
583
- // Convert function call to action(s)
584
- const action = this.convertFunctionCallToAction(part.functionCall, logger);
585
- if (action) {
586
- // Special handling for type actions. gemini-2.5 type_text_at carries
587
- // coordinates (click the field first); gemini-3.x `type` has none and
588
- // types into the element the model already focused.
589
- if (action.type === "type") {
590
- logger({
591
- category: "agent",
592
- message: `Adding action: ${JSON.stringify(action)}`,
593
- level: 2,
594
- });
595
- // Click the target first only when coordinates are provided.
596
- if (typeof action.x === "number" && typeof action.y === "number") {
597
- actions.push({
598
- type: "click",
599
- x: action.x,
600
- y: action.y,
601
- button: "left",
602
- });
603
- }
604
- // If clear_before_typing is true (default), add a select all
605
- if (action.clearBeforeTyping) {
606
- // Select all text in the field
607
- actions.push({
608
- type: "keypress",
609
- keys: ["ControlOrMeta+A"],
610
- });
611
- actions.push({
612
- type: "keypress",
613
- keys: ["Backspace"],
614
- });
615
- }
616
- // Then add the type action
617
- actions.push(action);
618
- if (action.pressEnter) {
619
- actions.push({
620
- type: "keypress",
621
- keys: ["Enter"],
622
- });
623
- }
624
- }
625
- else {
626
- actions.push(action);
627
- }
628
- }
629
- else {
718
+ const action = this.convertFunctionCallToAction(functionCall, logger);
719
+ if (!action) {
630
720
  logger({
631
721
  category: "agent",
632
- message: `Warning: Could not convert function call ${part.functionCall.name} to action`,
722
+ message: `Warning: Could not convert function call ${functionCall.name} to action`,
633
723
  level: 1,
634
724
  });
725
+ calls.push({
726
+ functionCall,
727
+ actions: [],
728
+ error: `Unsupported or malformed computer-use action "${functionCall.name}" with args ${JSON.stringify(functionCall.args ?? {})}`,
729
+ });
730
+ continue;
635
731
  }
732
+ calls.push({ functionCall, actions: this.expandAction(action) });
636
733
  }
637
734
  }
735
+ const actions = calls.flatMap((call) => call.actions);
638
736
  // Log summary of what we found
639
737
  logger({
640
738
  category: "agent",
641
- message: `Found ${functionCalls.length} function calls, converted to ${actions.length} actions`,
739
+ message: `Found ${calls.length} function calls, converted to ${actions.length} actions`,
642
740
  level: 2,
643
741
  });
644
- // Check if task is completed
645
- const completed = functionCalls.length === 0 ||
646
- (candidate.finishReason && candidate.finishReason !== "STOP");
742
+ if (calls.length === 0 && !message.trim()) {
743
+ throw new sdkErrors_js_1.LLMResponseError("agent", "Response has neither function calls nor text");
744
+ }
745
+ // Early stops (non-STOP finish reasons) are rejected in executeStep, so a
746
+ // turn is complete exactly when the model made no function calls.
647
747
  return {
648
748
  actions,
649
749
  message: message.trim(),
650
- completed,
651
- functionCalls,
750
+ completed: calls.length === 0,
751
+ calls,
652
752
  };
653
753
  }
754
+ /**
755
+ * Expand a converted action into the executor steps it needs. Typing may
756
+ * click the field first, clear it, and press Enter afterwards.
757
+ */
758
+ expandAction(action) {
759
+ if (action.type !== "type") {
760
+ return [action];
761
+ }
762
+ const steps = [];
763
+ // Click the target first only when coordinates are provided.
764
+ if (typeof action.x === "number" && typeof action.y === "number") {
765
+ steps.push({ type: "click", x: action.x, y: action.y, button: "left" });
766
+ }
767
+ if (action.clearBeforeTyping) {
768
+ steps.push({ type: "keypress", keys: ["ControlOrMeta+A"] });
769
+ steps.push({ type: "keypress", keys: ["Backspace"] });
770
+ }
771
+ steps.push(action);
772
+ if (action.pressEnter) {
773
+ steps.push({ type: "keypress", keys: ["Enter"] });
774
+ }
775
+ return steps;
776
+ }
654
777
  /**
655
778
  * Convert Google function call to Stagehand action
656
779
  */
@@ -692,7 +815,6 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
692
815
  press_keys: "key_combination",
693
816
  hotkey: "key_combination",
694
817
  screenshot: "take_screenshot",
695
- wait: "wait_5_seconds",
696
818
  };
697
819
  const name = NAME_ALIASES[rawName] ?? rawName;
698
820
  // Predefined computer-use tools take precedence over custom tools. If a
@@ -764,15 +886,37 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
764
886
  button: name === "right_click" ? "right" : "middle",
765
887
  };
766
888
  }
889
+ // gemini-3.x press/release primitives (e.g. a manual drag or a
890
+ // modifier held across clicks).
891
+ case "mouse_down":
892
+ case "mouse_up": {
893
+ if (!GoogleCUAClient.isFiniteCoord(args.x) ||
894
+ !GoogleCUAClient.isFiniteCoord(args.y)) {
895
+ return null;
896
+ }
897
+ const { x, y } = this.normalizeCoordinates(args.x, args.y);
898
+ return { type: name, x, y };
899
+ }
900
+ case "key_down":
901
+ case "key_up": {
902
+ if (typeof args.key !== "string" || !args.key.trim()) {
903
+ return null;
904
+ }
905
+ return { type: name, key: args.key.trim() };
906
+ }
767
907
  case "type_text_at": {
768
908
  // text is required; reject a malformed call rather than typing
769
909
  // "undefined". An empty string is valid (e.g. clear the field).
770
910
  if (typeof args.text !== "string") {
771
911
  return null;
772
912
  }
773
- // press_enter and clear_before_typing are shared across generations.
913
+ // gemini-2.5 type_text_at clears the field by default
914
+ // (clear_before_typing). gemini-3.x `type` has no such arg and types
915
+ // into the focused element, so only clear when asked or when the call
916
+ // targets a field by coordinates.
917
+ const hasCoords = typeof args.x === "number" && typeof args.y === "number";
774
918
  const pressEnter = args.press_enter ?? false;
775
- const clearBeforeTyping = args.clear_before_typing ?? true;
919
+ const clearBeforeTyping = args.clear_before_typing ?? hasCoords;
776
920
  const base = {
777
921
  type: "type",
778
922
  text: args.text,
@@ -783,7 +927,7 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
783
927
  // gemini-3.x `type` has no coordinates and types into the element the
784
928
  // model already focused with a preceding click. Only attach coords when
785
929
  // present so the executor doesn't click at NaN.
786
- if (typeof args.x === "number" && typeof args.y === "number") {
930
+ if (hasCoords) {
787
931
  const { x, y } = this.normalizeCoordinates(args.x, args.y);
788
932
  return { ...base, x, y };
789
933
  }
@@ -829,12 +973,15 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
829
973
  }
830
974
  const { x, y } = this.normalizeCoordinates(args.x, args.y);
831
975
  const direction = (args.direction || "down").toLowerCase();
832
- // 2.5 uses `magnitude`; gemini-3.x `scroll` uses `magnitude_in_pixels`.
976
+ // 2.5 uses `magnitude` (default 800); gemini-3.x `scroll` uses
977
+ // `magnitude_in_pixels` (default 300).
833
978
  const magnitude = GoogleCUAClient.isFiniteCoord(args.magnitude)
834
979
  ? args.magnitude
835
980
  : GoogleCUAClient.isFiniteCoord(args.magnitude_in_pixels)
836
981
  ? args.magnitude_in_pixels
837
- : 800;
982
+ : rawName === "scroll"
983
+ ? 300
984
+ : 800;
838
985
  let scroll_x = 0;
839
986
  let scroll_y = 0;
840
987
  if (direction === "up") {
@@ -884,6 +1031,16 @@ class GoogleCUAClient extends AgentClient_js_1.AgentClient {
884
1031
  type: "wait",
885
1032
  timeMs: 5000, // Google CUA waits for 5 seconds
886
1033
  };
1034
+ case "wait": {
1035
+ // gemini-3.x `wait` takes optional `seconds` (default 1).
1036
+ const seconds = GoogleCUAClient.isFiniteCoord(args.seconds) && args.seconds >= 0
1037
+ ? args.seconds
1038
+ : 1;
1039
+ return {
1040
+ type: "wait",
1041
+ timeMs: Math.round(seconds * 1000),
1042
+ };
1043
+ }
887
1044
  case "hover_at": {
888
1045
  const { x, y } = this.normalizeCoordinates(args.x, args.y);
889
1046
  return {