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