@loadmill/droid-cua 2.4.0 → 2.6.0
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/build/src/core/execution-engine.js +380 -130
- package/build/src/device/android/actions.js +16 -8
- package/build/src/device/assertions.js +6 -4
- package/build/src/device/cloud/actions.js +18 -9
- package/build/src/device/ios/actions.js +18 -10
- package/build/src/device/openai.js +80 -46
- package/build/src/device/screenshot-resolution.js +33 -0
- package/build/src/device/scroll-gesture.js +20 -0
- package/build/src/modes/execution-mode.js +36 -3
- package/build/src/prompts/base.js +4 -0
- package/build/src/test-store/test-manager.js +3 -5
- package/build/src/test-store/test-script.js +50 -0
- package/build/src/utils/cua-debug-tracer.js +48 -5
- package/build/src/utils/run-screenshot-recorder.js +161 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import path from "path";
|
|
2
3
|
import { writeFile } from "fs/promises";
|
|
3
4
|
import { getScreenshotAsBase64, getCurrentPlatform } from "../device/connection.js";
|
|
@@ -6,31 +7,99 @@ import { sendCUARequest } from "../device/openai.js";
|
|
|
6
7
|
import { emitDesktopDebug } from "../utils/desktop-debug.js";
|
|
7
8
|
import { getConfiguredStepDelayMs } from "../utils/step-delay.js";
|
|
8
9
|
import { printCliOutput } from "../utils/console-output.js";
|
|
9
|
-
|
|
10
|
+
import { annotateScreenshotArtifact, createAnnotatedScreenshotFromBase64 } from "../utils/run-screenshot-recorder.js";
|
|
11
|
+
function extractActionableOutputItems(items) {
|
|
10
12
|
const entries = [];
|
|
11
13
|
for (const item of items) {
|
|
12
|
-
if (item?.type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if (item?.type === "function_call" && item.name === "preview_click") {
|
|
15
|
+
entries.push({
|
|
16
|
+
type: "preview_click",
|
|
17
|
+
call_id: item.call_id || item.id,
|
|
18
|
+
arguments: item.arguments,
|
|
19
|
+
});
|
|
16
20
|
continue;
|
|
17
|
-
const actions = [];
|
|
18
|
-
if (Array.isArray(item.actions)) {
|
|
19
|
-
for (const actionEntry of item.actions) {
|
|
20
|
-
if (!actionEntry)
|
|
21
|
-
continue;
|
|
22
|
-
const action = actionEntry.action || actionEntry;
|
|
23
|
-
if (action?.type)
|
|
24
|
-
actions.push(action);
|
|
25
|
-
}
|
|
26
21
|
}
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
if (item?.type === "computer_call") {
|
|
23
|
+
const baseCallId = item.call_id || item.id;
|
|
24
|
+
if (!baseCallId)
|
|
25
|
+
continue;
|
|
26
|
+
const actions = [];
|
|
27
|
+
if (Array.isArray(item.actions)) {
|
|
28
|
+
for (const actionEntry of item.actions) {
|
|
29
|
+
if (!actionEntry)
|
|
30
|
+
continue;
|
|
31
|
+
const action = actionEntry.action || actionEntry;
|
|
32
|
+
if (action?.type)
|
|
33
|
+
actions.push(action);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (item.action?.type) {
|
|
37
|
+
actions.push(item.action);
|
|
38
|
+
}
|
|
39
|
+
entries.push({ type: "computer_call", call_id: baseCallId, actions });
|
|
29
40
|
}
|
|
30
|
-
entries.push({ call_id: baseCallId, actions });
|
|
31
41
|
}
|
|
32
42
|
return entries;
|
|
33
43
|
}
|
|
44
|
+
function buildPreviewClickValidationError(argumentsText) {
|
|
45
|
+
if (!argumentsText || typeof argumentsText !== "string") {
|
|
46
|
+
return "Invalid preview_click arguments: expected a JSON object with numeric x and y.";
|
|
47
|
+
}
|
|
48
|
+
return `Invalid preview_click arguments: ${argumentsText}`;
|
|
49
|
+
}
|
|
50
|
+
function parsePreviewClickArguments(argumentsText) {
|
|
51
|
+
let parsedArguments;
|
|
52
|
+
try {
|
|
53
|
+
parsedArguments = JSON.parse(argumentsText);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: buildPreviewClickValidationError("arguments could not be parsed as JSON; expected numeric x and y."),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const x = parsedArguments?.x;
|
|
62
|
+
const y = parsedArguments?.y;
|
|
63
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
message: buildPreviewClickValidationError("x and y must both be numeric."),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
x,
|
|
72
|
+
y,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildFunctionCallOutputItem(callId, output) {
|
|
76
|
+
return {
|
|
77
|
+
type: "function_call_output",
|
|
78
|
+
call_id: callId,
|
|
79
|
+
output,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildPreviewOutputText({ requestedX, requestedY, renderedX, renderedY, wasClamped }) {
|
|
83
|
+
const baseText = `Marker placed at (${renderedX}, ${renderedY}) on the current screen. The click has not been executed.`;
|
|
84
|
+
if (!wasClamped) {
|
|
85
|
+
return baseText;
|
|
86
|
+
}
|
|
87
|
+
return `${baseText} Requested point (${requestedX}, ${requestedY}) was off-screen; marker rendered at (${renderedX}, ${renderedY}).`;
|
|
88
|
+
}
|
|
89
|
+
function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
|
|
90
|
+
const content = [{
|
|
91
|
+
type: "input_text",
|
|
92
|
+
text,
|
|
93
|
+
}];
|
|
94
|
+
if (annotatedScreenshotBase64) {
|
|
95
|
+
content.push({
|
|
96
|
+
type: "input_image",
|
|
97
|
+
image_url: `data:image/png;base64,${annotatedScreenshotBase64}`,
|
|
98
|
+
detail: "high",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
34
103
|
function getScopeAndIds(context = null, stepContext = null) {
|
|
35
104
|
const scope = context?.sessionId ? "design" : "execution";
|
|
36
105
|
const ids = scope === "design"
|
|
@@ -80,6 +149,37 @@ function buildStrictModePlan(actions = [], strictMode = false) {
|
|
|
80
149
|
runtimeNote
|
|
81
150
|
};
|
|
82
151
|
}
|
|
152
|
+
function normalizeArtifactPaths(result) {
|
|
153
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
154
|
+
if (Array.isArray(result.annotationSourcePaths)) {
|
|
155
|
+
return normalizeArtifactPaths(result.annotationSourcePaths);
|
|
156
|
+
}
|
|
157
|
+
return normalizeArtifactPaths(result.artifactPaths);
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(result)) {
|
|
160
|
+
return result.filter((value) => typeof value === "string" && value.trim());
|
|
161
|
+
}
|
|
162
|
+
if (typeof result === "string" && result.trim()) {
|
|
163
|
+
return [result];
|
|
164
|
+
}
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
function screenshotPathFromRecorderResult(result) {
|
|
168
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
169
|
+
return typeof result.screenshotPath === "string" && result.screenshotPath.trim()
|
|
170
|
+
? result.screenshotPath
|
|
171
|
+
: null;
|
|
172
|
+
}
|
|
173
|
+
return typeof result === "string" && result.trim() ? result : null;
|
|
174
|
+
}
|
|
175
|
+
function screenshotDataUrlFromRecorderResult(result) {
|
|
176
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
177
|
+
return typeof result.screenshotDataUrl === "string" && result.screenshotDataUrl.trim()
|
|
178
|
+
? result.screenshotDataUrl
|
|
179
|
+
: null;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
83
183
|
export class ExecutionEngine {
|
|
84
184
|
constructor(session, options = {}) {
|
|
85
185
|
this.session = session;
|
|
@@ -89,26 +189,45 @@ export class ExecutionEngine {
|
|
|
89
189
|
this.strictMode = options.strictMode === true;
|
|
90
190
|
this.stepDelayMs = getConfiguredStepDelayMs();
|
|
91
191
|
this.reportedScreenshotWriteError = false;
|
|
192
|
+
this.reportedScreenshotAnnotateError = false;
|
|
193
|
+
this.lastRecordedScreenshotPaths = [];
|
|
92
194
|
this.getScreenshotAsBase64 = options.getScreenshotAsBase64 || getScreenshotAsBase64;
|
|
93
195
|
this.handleModelAction = options.handleModelAction || handleModelAction;
|
|
94
196
|
this.sendCUARequest = options.sendCUARequest || sendCUARequest;
|
|
95
197
|
this.getCurrentPlatform = options.getCurrentPlatform || getCurrentPlatform;
|
|
96
198
|
}
|
|
97
|
-
async
|
|
199
|
+
async saveScreenshotArtifact(screenshotBase64, metadata = {}, options = {}) {
|
|
98
200
|
if (typeof screenshotBase64 !== "string" || !screenshotBase64) {
|
|
99
201
|
return null;
|
|
100
202
|
}
|
|
203
|
+
const updateLastRecordedScreenshotPaths = options.updateLastRecordedScreenshotPaths !== false;
|
|
204
|
+
const previousRecordedScreenshotPaths = updateLastRecordedScreenshotPaths
|
|
205
|
+
? null
|
|
206
|
+
: [...this.lastRecordedScreenshotPaths];
|
|
101
207
|
try {
|
|
102
208
|
if (this.screenshotRecorder?.saveScreenshot) {
|
|
103
|
-
|
|
209
|
+
const result = await this.screenshotRecorder.saveScreenshot(screenshotBase64, metadata);
|
|
210
|
+
if (updateLastRecordedScreenshotPaths) {
|
|
211
|
+
this.lastRecordedScreenshotPaths = normalizeArtifactPaths(result);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
104
214
|
}
|
|
105
215
|
if (this.recordScreenshots && this.screenshotDir) {
|
|
106
216
|
const framePath = path.join(this.screenshotDir, `frame_${String(Date.now())}.png`);
|
|
107
217
|
await writeFile(framePath, Buffer.from(screenshotBase64, "base64"));
|
|
218
|
+
if (updateLastRecordedScreenshotPaths) {
|
|
219
|
+
this.lastRecordedScreenshotPaths = [framePath];
|
|
220
|
+
}
|
|
108
221
|
return framePath;
|
|
109
222
|
}
|
|
110
223
|
}
|
|
111
224
|
catch (error) {
|
|
225
|
+
if (updateLastRecordedScreenshotPaths) {
|
|
226
|
+
this.lastRecordedScreenshotPaths = [];
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.lastRecordedScreenshotPaths = previousRecordedScreenshotPaths;
|
|
230
|
+
}
|
|
112
231
|
if (!this.reportedScreenshotWriteError) {
|
|
113
232
|
this.reportedScreenshotWriteError = true;
|
|
114
233
|
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
@@ -131,6 +250,100 @@ export class ExecutionEngine {
|
|
|
131
250
|
}
|
|
132
251
|
return null;
|
|
133
252
|
}
|
|
253
|
+
async recordScreenshot(screenshotBase64, metadata = {}) {
|
|
254
|
+
return await this.saveScreenshotArtifact(screenshotBase64, metadata);
|
|
255
|
+
}
|
|
256
|
+
async recordPreviewScreenshot(screenshotBase64, metadata = {}) {
|
|
257
|
+
return await this.saveScreenshotArtifact(screenshotBase64, metadata, {
|
|
258
|
+
updateLastRecordedScreenshotPaths: false,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async annotateLatestScreenshot(action, metadata = {}) {
|
|
262
|
+
if (!(action?.type === "click" || action?.type === "double_click") || this.lastRecordedScreenshotPaths.length === 0) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const marker = {
|
|
267
|
+
x: action.x,
|
|
268
|
+
y: action.y,
|
|
269
|
+
style: "click",
|
|
270
|
+
};
|
|
271
|
+
const sourcePath = this.lastRecordedScreenshotPaths.find((filePath) => typeof filePath === "string" && filePath.trim());
|
|
272
|
+
if (sourcePath && this.screenshotRecorder?.saveAnnotatedScreenshot) {
|
|
273
|
+
return await this.screenshotRecorder.saveAnnotatedScreenshot(sourcePath, marker, metadata);
|
|
274
|
+
}
|
|
275
|
+
await Promise.all(this.lastRecordedScreenshotPaths.map((filePath) => annotateScreenshotArtifact(filePath, marker)));
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (!this.reportedScreenshotAnnotateError) {
|
|
280
|
+
this.reportedScreenshotAnnotateError = true;
|
|
281
|
+
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
282
|
+
const ids = scope === "design"
|
|
283
|
+
? {
|
|
284
|
+
sessionId: metadata.sessionId,
|
|
285
|
+
stepId: metadata.stepId,
|
|
286
|
+
instructionIndex: metadata.instructionIndex
|
|
287
|
+
}
|
|
288
|
+
: {
|
|
289
|
+
runId: metadata.runId,
|
|
290
|
+
stepId: metadata.stepId,
|
|
291
|
+
instructionIndex: metadata.instructionIndex
|
|
292
|
+
};
|
|
293
|
+
emitDesktopDebug("run.screenshot_annotate.error", scope, ids, {
|
|
294
|
+
actionType: action?.type ?? null,
|
|
295
|
+
x: action?.x ?? null,
|
|
296
|
+
y: action?.y ?? null,
|
|
297
|
+
message: error instanceof Error ? error.message : "Failed to annotate screenshot"
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
async handlePreviewClick(callId, argumentsText, context = null, stepContext = null) {
|
|
304
|
+
const parsedArguments = parsePreviewClickArguments(argumentsText);
|
|
305
|
+
if (!parsedArguments.ok) {
|
|
306
|
+
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({ text: parsedArguments.message }));
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
310
|
+
const previewResult = await createAnnotatedScreenshotFromBase64(screenshotBase64, {
|
|
311
|
+
x: parsedArguments.x,
|
|
312
|
+
y: parsedArguments.y,
|
|
313
|
+
style: "preview",
|
|
314
|
+
});
|
|
315
|
+
if (!previewResult?.screenshotBase64 || !previewResult?.marker) {
|
|
316
|
+
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
317
|
+
text: "preview_click failed: could not annotate the current screenshot.",
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
const previewEventId = randomUUID();
|
|
321
|
+
await this.recordPreviewScreenshot(previewResult.screenshotBase64, {
|
|
322
|
+
runId: context?.runId,
|
|
323
|
+
sessionId: context?.sessionId,
|
|
324
|
+
stepId: stepContext?.stepId,
|
|
325
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
326
|
+
callId,
|
|
327
|
+
eventId: previewEventId,
|
|
328
|
+
captureSource: "preview-click",
|
|
329
|
+
});
|
|
330
|
+
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
331
|
+
text: buildPreviewOutputText({
|
|
332
|
+
requestedX: previewResult.marker.requestedX,
|
|
333
|
+
requestedY: previewResult.marker.requestedY,
|
|
334
|
+
renderedX: previewResult.marker.x,
|
|
335
|
+
renderedY: previewResult.marker.y,
|
|
336
|
+
wasClamped: previewResult.marker.wasClamped,
|
|
337
|
+
}),
|
|
338
|
+
annotatedScreenshotBase64: previewResult.screenshotBase64,
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
343
|
+
text: `preview_click failed: ${error instanceof Error ? error.message : "unable to capture or annotate the current screen."}`,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
134
347
|
/**
|
|
135
348
|
* Run a full turn with the CUA model
|
|
136
349
|
* Executes actions until the model stops requesting actions
|
|
@@ -156,7 +369,7 @@ export class ExecutionEngine {
|
|
|
156
369
|
return newResponseId;
|
|
157
370
|
}
|
|
158
371
|
const items = response.output || [];
|
|
159
|
-
const
|
|
372
|
+
const actionableItems = extractActionableOutputItems(items);
|
|
160
373
|
// ── Collect pending safety checks ──
|
|
161
374
|
const pendingSafetyChecks = items
|
|
162
375
|
.filter(item => item.type === "pending_safety_check")
|
|
@@ -203,117 +416,18 @@ export class ExecutionEngine {
|
|
|
203
416
|
});
|
|
204
417
|
}
|
|
205
418
|
}
|
|
206
|
-
if (
|
|
419
|
+
if (actionableItems.length === 0) {
|
|
207
420
|
// No actions = turn complete
|
|
208
421
|
break;
|
|
209
422
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return newResponseId;
|
|
219
|
-
}
|
|
220
|
-
if (action.type === "screenshot") {
|
|
221
|
-
sawExplicitScreenshotAction = true;
|
|
222
|
-
addOutput({
|
|
223
|
-
type: 'info',
|
|
224
|
-
text: 'Capturing screen',
|
|
225
|
-
eventType: 'screenshot_captured',
|
|
226
|
-
actionType: 'screenshot',
|
|
227
|
-
...eventMeta({
|
|
228
|
-
payload: {
|
|
229
|
-
callId: call_id,
|
|
230
|
-
source: 'explicit_action'
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
237
|
-
...context,
|
|
238
|
-
shouldStop,
|
|
239
|
-
stepId: stepContext?.stepId,
|
|
240
|
-
instructionIndex: stepContext?.instructionIndex
|
|
241
|
-
});
|
|
242
|
-
// Track action and check for interruption
|
|
243
|
-
if (trackAction) {
|
|
244
|
-
trackAction(action);
|
|
245
|
-
}
|
|
246
|
-
if (shouldStop()) {
|
|
247
|
-
// User interrupted - stop execution immediately
|
|
248
|
-
return newResponseId;
|
|
249
|
-
}
|
|
250
|
-
// Add delay after UI-changing actions to let the interface update
|
|
251
|
-
// before taking the screenshot (except for explicit wait actions which have their own delay)
|
|
252
|
-
if (action.type !== "wait") {
|
|
253
|
-
await new Promise(resolve => setTimeout(resolve, this.stepDelayMs));
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (shouldStop()) {
|
|
258
|
-
return newResponseId;
|
|
259
|
-
}
|
|
260
|
-
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
261
|
-
emitDesktopDebug("device.screenshot", "device", {
|
|
262
|
-
runId: context?.runId,
|
|
263
|
-
stepId: stepContext?.stepId,
|
|
264
|
-
instructionIndex: stepContext?.instructionIndex
|
|
265
|
-
}, {
|
|
266
|
-
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
267
|
-
callId: call_id,
|
|
268
|
-
width: this.session.deviceInfo?.scaled_width,
|
|
269
|
-
height: this.session.deviceInfo?.scaled_height,
|
|
270
|
-
base64Length: screenshotBase64.length
|
|
271
|
-
});
|
|
272
|
-
await this.recordScreenshot(screenshotBase64, {
|
|
273
|
-
runId: context?.runId,
|
|
274
|
-
sessionId: context?.sessionId,
|
|
275
|
-
stepId: stepContext?.stepId,
|
|
276
|
-
instructionIndex: stepContext?.instructionIndex,
|
|
277
|
-
callId: call_id,
|
|
278
|
-
captureSource: sawExplicitScreenshotAction ? "call-output-explicit-action" : "call-output-post-action"
|
|
279
|
-
});
|
|
280
|
-
if (runtimeNote) {
|
|
281
|
-
const executedActionTypes = actionsToExecute
|
|
282
|
-
.map((action) => action?.type)
|
|
283
|
-
.filter(Boolean);
|
|
284
|
-
const droppedActionTypes = droppedActions
|
|
285
|
-
.map((action) => action?.type)
|
|
286
|
-
.filter(Boolean);
|
|
287
|
-
emitDesktopDebug("cua.strict_mode.truncation", scope, ids, {
|
|
288
|
-
callId: call_id,
|
|
289
|
-
executedCount: actionsToExecute.length,
|
|
290
|
-
droppedCount: droppedActions.length,
|
|
291
|
-
executedActionTypes,
|
|
292
|
-
droppedActionTypes,
|
|
293
|
-
reason: truncationReason,
|
|
294
|
-
note: runtimeNote
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
// Build next input: screenshot + any carryover reasoning
|
|
298
|
-
const selectedCuaModel = process.env.OPENAI_CUA_MODEL === "computer-use-preview" ? "computer-use-preview" : "gpt-5.4";
|
|
299
|
-
const input = [{
|
|
300
|
-
type: "computer_call_output",
|
|
301
|
-
call_id,
|
|
302
|
-
output: {
|
|
303
|
-
type: "computer_screenshot",
|
|
304
|
-
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
305
|
-
},
|
|
306
|
-
...(selectedCuaModel === "computer-use-preview"
|
|
307
|
-
? { current_url: this.getCurrentPlatform() === "ios" ? "ios://simulator" : "android://device" }
|
|
308
|
-
: {}),
|
|
309
|
-
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
310
|
-
}];
|
|
311
|
-
if (runtimeNote) {
|
|
312
|
-
input.push({
|
|
313
|
-
role: "user",
|
|
314
|
-
content: runtimeNote
|
|
315
|
-
});
|
|
316
|
-
}
|
|
423
|
+
const actionableItem = actionableItems[0];
|
|
424
|
+
if (!actionableItem?.call_id) {
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (actionableItem.type === "preview_click") {
|
|
428
|
+
const input = [
|
|
429
|
+
await this.handlePreviewClick(actionableItem.call_id, actionableItem.arguments, context, stepContext),
|
|
430
|
+
];
|
|
317
431
|
if (shouldStop()) {
|
|
318
432
|
return newResponseId;
|
|
319
433
|
}
|
|
@@ -330,9 +444,145 @@ export class ExecutionEngine {
|
|
|
330
444
|
}
|
|
331
445
|
});
|
|
332
446
|
newResponseId = response.id;
|
|
333
|
-
|
|
334
|
-
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const { call_id, actions } = actionableItem;
|
|
450
|
+
const { actionsToExecute, droppedActions, truncationReason, runtimeNote } = buildStrictModePlan(actions, this.strictMode);
|
|
451
|
+
let sawExplicitScreenshotAction = false;
|
|
452
|
+
for (const action of actionsToExecute) {
|
|
453
|
+
const actionEventId = randomUUID();
|
|
454
|
+
if (shouldStop()) {
|
|
455
|
+
return newResponseId;
|
|
456
|
+
}
|
|
457
|
+
if (action.type === "screenshot") {
|
|
458
|
+
sawExplicitScreenshotAction = true;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const annotatedScreenshotResult = await this.annotateLatestScreenshot(action, {
|
|
462
|
+
runId: context?.runId,
|
|
463
|
+
sessionId: context?.sessionId,
|
|
464
|
+
stepId: stepContext?.stepId,
|
|
465
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
466
|
+
eventId: actionEventId,
|
|
467
|
+
captureSource: "action-click-annotation"
|
|
468
|
+
});
|
|
469
|
+
const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
|
|
470
|
+
const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
|
|
471
|
+
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
472
|
+
...context,
|
|
473
|
+
shouldStop,
|
|
474
|
+
stepId: stepContext?.stepId,
|
|
475
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
476
|
+
eventId: actionEventId,
|
|
477
|
+
...(annotatedScreenshotPath ? { screenshotPath: annotatedScreenshotPath } : {}),
|
|
478
|
+
...(context?.includeScreenshotDataUrl && annotatedScreenshotDataUrl ? { screenshotDataUrl: annotatedScreenshotDataUrl } : {})
|
|
479
|
+
});
|
|
480
|
+
if (trackAction) {
|
|
481
|
+
trackAction(action);
|
|
482
|
+
}
|
|
483
|
+
if (shouldStop()) {
|
|
484
|
+
return newResponseId;
|
|
485
|
+
}
|
|
486
|
+
if (action.type !== "wait") {
|
|
487
|
+
await new Promise(resolve => setTimeout(resolve, this.stepDelayMs));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
335
490
|
}
|
|
491
|
+
if (shouldStop()) {
|
|
492
|
+
return newResponseId;
|
|
493
|
+
}
|
|
494
|
+
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
495
|
+
emitDesktopDebug("device.screenshot", "device", {
|
|
496
|
+
runId: context?.runId,
|
|
497
|
+
stepId: stepContext?.stepId,
|
|
498
|
+
instructionIndex: stepContext?.instructionIndex
|
|
499
|
+
}, {
|
|
500
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
501
|
+
callId: call_id,
|
|
502
|
+
width: this.session.deviceInfo?.scaled_width,
|
|
503
|
+
height: this.session.deviceInfo?.scaled_height,
|
|
504
|
+
base64Length: screenshotBase64.length
|
|
505
|
+
});
|
|
506
|
+
const screenshotEventId = randomUUID();
|
|
507
|
+
const captureSource = sawExplicitScreenshotAction ? "call-output-explicit-action" : "call-output-post-action";
|
|
508
|
+
const screenshotResult = await this.recordScreenshot(screenshotBase64, {
|
|
509
|
+
runId: context?.runId,
|
|
510
|
+
sessionId: context?.sessionId,
|
|
511
|
+
stepId: stepContext?.stepId,
|
|
512
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
513
|
+
eventId: screenshotEventId,
|
|
514
|
+
captureSource
|
|
515
|
+
});
|
|
516
|
+
const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
|
|
517
|
+
addOutput({
|
|
518
|
+
type: 'info',
|
|
519
|
+
text: 'Capturing screen',
|
|
520
|
+
eventType: 'screenshot_captured',
|
|
521
|
+
actionType: 'screenshot',
|
|
522
|
+
...eventMeta({
|
|
523
|
+
payload: {
|
|
524
|
+
eventId: screenshotEventId,
|
|
525
|
+
captureSource,
|
|
526
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
527
|
+
...(typeof screenshotPath === "string" && screenshotPath ? { screenshotPath } : {}),
|
|
528
|
+
...(context?.includeScreenshotDataUrl
|
|
529
|
+
? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
|
|
530
|
+
: {})
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
});
|
|
534
|
+
if (runtimeNote) {
|
|
535
|
+
const executedActionTypes = actionsToExecute
|
|
536
|
+
.map((action) => action?.type)
|
|
537
|
+
.filter(Boolean);
|
|
538
|
+
const droppedActionTypes = droppedActions
|
|
539
|
+
.map((action) => action?.type)
|
|
540
|
+
.filter(Boolean);
|
|
541
|
+
emitDesktopDebug("cua.strict_mode.truncation", scope, ids, {
|
|
542
|
+
callId: call_id,
|
|
543
|
+
executedCount: actionsToExecute.length,
|
|
544
|
+
droppedCount: droppedActions.length,
|
|
545
|
+
executedActionTypes,
|
|
546
|
+
droppedActionTypes,
|
|
547
|
+
reason: truncationReason,
|
|
548
|
+
note: runtimeNote
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
const selectedCuaModel = process.env.OPENAI_CUA_MODEL === "computer-use-preview" ? "computer-use-preview" : "gpt-5.4";
|
|
552
|
+
const input = [{
|
|
553
|
+
type: "computer_call_output",
|
|
554
|
+
call_id,
|
|
555
|
+
output: {
|
|
556
|
+
type: "computer_screenshot",
|
|
557
|
+
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
558
|
+
},
|
|
559
|
+
...(selectedCuaModel === "computer-use-preview"
|
|
560
|
+
? { current_url: this.getCurrentPlatform() === "ios" ? "ios://simulator" : "android://device" }
|
|
561
|
+
: {}),
|
|
562
|
+
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
563
|
+
}];
|
|
564
|
+
if (runtimeNote) {
|
|
565
|
+
input.push({
|
|
566
|
+
role: "user",
|
|
567
|
+
content: runtimeNote
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (shouldStop()) {
|
|
571
|
+
return newResponseId;
|
|
572
|
+
}
|
|
573
|
+
response = await this.sendCUARequest({
|
|
574
|
+
messages: input,
|
|
575
|
+
previousResponseId: newResponseId,
|
|
576
|
+
deviceInfo: this.session.deviceInfo,
|
|
577
|
+
debugContext: {
|
|
578
|
+
scope,
|
|
579
|
+
runId: context?.runId,
|
|
580
|
+
sessionId: context?.sessionId,
|
|
581
|
+
stepId: stepContext?.stepId,
|
|
582
|
+
instructionIndex: stepContext?.instructionIndex
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
newResponseId = response.id;
|
|
336
586
|
}
|
|
337
587
|
// ── At end, if last output was only reasoning ──
|
|
338
588
|
const finalItems = response.output || [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from "../../utils/logger.js";
|
|
2
2
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
3
3
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
4
|
+
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
4
5
|
import { execAdb } from "./tools.js";
|
|
5
6
|
function adbShell(deviceId, command) {
|
|
6
7
|
return execAdb(["-s", deviceId, "shell", command]);
|
|
@@ -39,6 +40,9 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
39
40
|
stepId: context?.stepId,
|
|
40
41
|
instructionIndex: context?.instructionIndex,
|
|
41
42
|
payload: {
|
|
43
|
+
eventId: context?.eventId,
|
|
44
|
+
screenshotPath: context?.screenshotPath,
|
|
45
|
+
screenshotDataUrl: context?.screenshotDataUrl,
|
|
42
46
|
platform: 'android',
|
|
43
47
|
...payload
|
|
44
48
|
}
|
|
@@ -70,17 +74,20 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
70
74
|
case "double_click":
|
|
71
75
|
const realX = Math.round(x / scale);
|
|
72
76
|
const realY = Math.round(y / scale);
|
|
73
|
-
addOutput({ type: 'action', text:
|
|
77
|
+
addOutput({ type: 'action', text: 'Click', ...meta({ x: realX, y: realY, unit: 'px' }) });
|
|
74
78
|
await adbShell(deviceId, `input tap ${realX} ${realY}`);
|
|
75
79
|
break;
|
|
76
80
|
case "scroll":
|
|
77
|
-
const scrollX =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
const { scrollX, scrollY, startX, startY, endX, endY, hasAnchor } = resolveScrollGesture(action, {
|
|
82
|
+
scale,
|
|
83
|
+
fallbackStartX: 500,
|
|
84
|
+
fallbackStartY: 500
|
|
85
|
+
});
|
|
86
|
+
addOutput({
|
|
87
|
+
type: 'action',
|
|
88
|
+
text: `Scrolling from (${startX}, ${startY}) to (${endX}, ${endY}) by (${scrollX}, ${scrollY})`,
|
|
89
|
+
...meta({ scrollX, scrollY, startX, startY, endX, endY, anchorSource: hasAnchor ? 'action' : 'fallback', unit: 'px' })
|
|
90
|
+
});
|
|
84
91
|
await adbShell(deviceId, `input swipe ${startX} ${startY} ${endX} ${endY} 500`);
|
|
85
92
|
break;
|
|
86
93
|
case "drag":
|
|
@@ -150,6 +157,7 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
150
157
|
stepId: context?.stepId,
|
|
151
158
|
instructionIndex: context?.instructionIndex,
|
|
152
159
|
payload: {
|
|
160
|
+
eventId: context?.eventId,
|
|
153
161
|
message: error.message,
|
|
154
162
|
platform: 'android'
|
|
155
163
|
}
|