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