@loadmill/droid-cua 2.18.0 → 2.19.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.
@@ -185,6 +185,19 @@ export class ExecutionEngine {
185
185
  if (shouldStop()) {
186
186
  return { stopped: true, sawExplicitScreenshotAction };
187
187
  }
188
+ if (context?.waitIfPaused) {
189
+ const pauseResult = await context.waitIfPaused({
190
+ boundary: "before_action",
191
+ refreshScreenOnResume: true,
192
+ stepContext,
193
+ });
194
+ if (pauseResult?.status === "stopped" || shouldStop()) {
195
+ return { stopped: true, sawExplicitScreenshotAction };
196
+ }
197
+ if (pauseResult?.refreshScreen) {
198
+ return { refreshModelState: true, sawExplicitScreenshotAction };
199
+ }
200
+ }
188
201
  if (action.type === "screenshot") {
189
202
  sawExplicitScreenshotAction = true;
190
203
  continue;
@@ -223,6 +236,17 @@ export class ExecutionEngine {
223
236
  }
224
237
  return { stopped: false, sawExplicitScreenshotAction };
225
238
  }
239
+ async waitIfPaused(context = null, stepContext = null, boundary = "turn_boundary", refreshScreenOnResume = true) {
240
+ if (!context?.waitIfPaused) {
241
+ return { status: "running", refreshScreen: false };
242
+ }
243
+ const result = await context.waitIfPaused({
244
+ boundary,
245
+ refreshScreenOnResume,
246
+ stepContext,
247
+ });
248
+ return result ?? { status: "running", refreshScreen: false };
249
+ }
226
250
  async buildActionFollowUp(callId, pendingSafetyChecks, sawExplicitScreenshotAction, addOutput, eventMeta, context = null, stepContext = null) {
227
251
  const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
228
252
  emitDesktopDebug("device.screenshot", "device", {
@@ -358,11 +382,21 @@ export class ExecutionEngine {
358
382
  instructionIndex: stepContext?.instructionIndex,
359
383
  ...extra
360
384
  });
385
+ let refreshBeforeNextComputerAction = false;
361
386
  while (true) {
362
387
  // Check for interruption before processing next batch of actions
363
388
  if (shouldStop()) {
389
+ summary.stopped = true;
390
+ return finalize();
391
+ }
392
+ const pauseResult = await this.waitIfPaused(context, stepContext, "before_model_output", true);
393
+ if (pauseResult.status === "stopped" || shouldStop()) {
394
+ summary.stopped = true;
364
395
  return finalize();
365
396
  }
397
+ if (pauseResult.refreshScreen) {
398
+ refreshBeforeNextComputerAction = true;
399
+ }
366
400
  const items = response.output || [];
367
401
  const actionableItems = extractActionableOutputItems(items);
368
402
  const pendingSafetyChecks = collectPendingSafetyChecks(items);
@@ -385,8 +419,17 @@ export class ExecutionEngine {
385
419
  currentScreenshot = toolResult.currentScreenshot;
386
420
  }
387
421
  if (shouldStop()) {
422
+ summary.stopped = true;
423
+ return finalize();
424
+ }
425
+ const toolPauseResult = await this.waitIfPaused(context, stepContext, "after_tool_call", true);
426
+ if (toolPauseResult.status === "stopped" || shouldStop()) {
427
+ summary.stopped = true;
388
428
  return finalize();
389
429
  }
430
+ if (toolPauseResult.refreshScreen) {
431
+ refreshBeforeNextComputerAction = true;
432
+ }
390
433
  if (toolResult.kind === "fail") {
391
434
  summary.failed = true;
392
435
  summary.error = toolResult.error;
@@ -419,14 +462,39 @@ export class ExecutionEngine {
419
462
  continue;
420
463
  }
421
464
  const { call_id, actions } = actionableItem;
465
+ if (refreshBeforeNextComputerAction) {
466
+ const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, true, addOutput, eventMeta, context, stepContext);
467
+ currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
468
+ response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
469
+ newResponseId = response.id;
470
+ refreshBeforeNextComputerAction = false;
471
+ continue;
472
+ }
422
473
  const actionBatchResult = await this.executeActionBatch(actions, currentScreenshot, shouldStop, context, stepContext, trackAction);
423
474
  if (actionBatchResult.stopped) {
475
+ summary.stopped = true;
424
476
  return finalize();
425
477
  }
478
+ if (actionBatchResult.refreshModelState) {
479
+ const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, true, addOutput, eventMeta, context, stepContext);
480
+ currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
481
+ response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
482
+ newResponseId = response.id;
483
+ continue;
484
+ }
426
485
  const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, actionBatchResult.sawExplicitScreenshotAction, addOutput, eventMeta, context, stepContext);
427
486
  if (shouldStop()) {
487
+ summary.stopped = true;
428
488
  return finalize();
429
489
  }
490
+ const followUpPauseResult = await this.waitIfPaused(context, stepContext, "after_action_follow_up", true);
491
+ if (followUpPauseResult.status === "stopped" || shouldStop()) {
492
+ summary.stopped = true;
493
+ return finalize();
494
+ }
495
+ if (followUpPauseResult.refreshScreen) {
496
+ refreshBeforeNextComputerAction = true;
497
+ }
430
498
  currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
431
499
  response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
432
500
  newResponseId = response.id;
@@ -32,6 +32,8 @@ export function mediaKindOf(line) {
32
32
  return "none";
33
33
  }
34
34
  export function isHiddenExecutionLogEvent(line) {
35
+ if (line.eventType === "run_paused")
36
+ return true;
35
37
  return mediaKindOf(line) === "hidden-debug";
36
38
  }
37
39
  export function isGalleryMediaEvent(line) {
@@ -145,6 +145,19 @@ export class ExecutionMode {
145
145
  });
146
146
  return { success: false, error: 'Stopped by user' };
147
147
  }
148
+ if (runContext.waitIfPaused) {
149
+ const pauseResult = await runContext.waitIfPaused({
150
+ boundary: 'before_instruction',
151
+ refreshScreenOnResume: false,
152
+ stepContext,
153
+ });
154
+ if (pauseResult?.status === 'stopped' || this.shouldStop) {
155
+ this.emit(addOutput, 'info', 'Test execution stopped by user.', runContext, stepContext, {
156
+ eventType: 'system_message'
157
+ });
158
+ return { success: false, error: 'Stopped by user' };
159
+ }
160
+ }
148
161
  const instruction = this.instructions[i];
149
162
  this.emit(addOutput, 'user', instruction, runContext, stepContext, {
150
163
  eventType: 'instruction_started',
@@ -256,6 +269,9 @@ export class ExecutionMode {
256
269
  return this.shouldStop;
257
270
  };
258
271
  const { responseId: newResponseId, summary: turnSummary } = await this.engine.runFullTurn(response, trackAction, instructionContext, stepContext, { currentScreenshot });
272
+ if (turnSummary.stopped || this.shouldStop) {
273
+ return { success: false, error: "Stopped by user" };
274
+ }
259
275
  if (turnSummary.assertionStatus === "passed") {
260
276
  this.stats.assertionsPassed++;
261
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadmill/droid-cua",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
5
5
  "main": "build/index.js",
6
6
  "type": "module",