@loadmill/droid-cua 2.9.0 → 2.10.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.
@@ -14,17 +14,17 @@ export async function handleHelp(args, session, context) {
14
14
  addOutput({ type: 'info', text: '' });
15
15
  addOutput({ type: 'info', text: 'Usage:' });
16
16
  addOutput({ type: 'info', text: ' droid-cua --avd <device-name> [options]' });
17
- addOutput({ type: 'info', text: ' droid-cua --target web --url <starting-url> --instructions <file> [options]' });
17
+ addOutput({ type: 'info', text: ' droid-cua --target web --instructions <file> [options]' });
18
18
  addOutput({ type: 'info', text: '' });
19
19
  addOutput({ type: 'info', text: 'Options:' });
20
20
  addOutput({ type: 'info', text: ' --target <kind> Target kind: mobile (default) or web' });
21
21
  addOutput({ type: 'info', text: ' --avd <name> Device name (Android device ID/serial or iOS Simulator)' });
22
22
  addOutput({ type: 'info', text: ' --platform <platform> Force platform: android or ios' });
23
23
  addOutput({ type: 'info', text: ' --instructions <file> Run test file in headless mode' });
24
- addOutput({ type: 'info', text: ' --url <value> Required starting URL for --target web headless runs' });
25
24
  addOutput({ type: 'info', text: ' --session-mode <mode> Browser session mode for web runs: persistent or fresh' });
26
25
  addOutput({ type: 'info', text: ' --config <file> Headless execution JSON config for prompt parity' });
27
26
  addOutput({ type: 'info', text: ' --cua-model <model> Headless CUA model override: gpt-5.4 or gpt-5.4-mini' });
27
+ addOutput({ type: 'info', text: ' --aim-assist <mode> Headless Aim Assist mode: off, smart, or always' });
28
28
  addOutput({ type: 'info', text: ' --context <file> Optional app context file injected into execution runs' });
29
29
  addOutput({ type: 'info', text: ' --no-context Disable app context for headless execution' });
30
30
  addOutput({ type: 'info', text: ' --context-optimization-threshold <tokens> Headless Context Optimization compaction threshold' });
@@ -33,8 +33,6 @@ export async function handleHelp(args, session, context) {
33
33
  addOutput({ type: 'info', text: ' --execution-prompt <text> Headless execution prompt customization text' });
34
34
  addOutput({ type: 'info', text: ' --base-prompt-file Headless base prompt customization file' });
35
35
  addOutput({ type: 'info', text: ' --execution-prompt-file Headless execution prompt customization file' });
36
- addOutput({ type: 'info', text: ' --strict Strict Mode: re-observe after the first action in a chain' });
37
- addOutput({ type: 'info', text: ' --no-strict Disable Strict Mode for headless config-driven runs' });
38
36
  addOutput({ type: 'info', text: ' --record Record screenshots during execution' });
39
37
  addOutput({ type: 'info', text: ' --debug Enable structured JSONL debug artifacts' });
40
38
  addOutput({ type: 'info', text: '' });
@@ -75,7 +73,7 @@ export async function handleHelp(args, session, context) {
75
73
  addOutput({ type: 'info', text: ' droid-cua --avd "iPhone 16" (iOS Simulator, auto-detected)' });
76
74
  addOutput({ type: 'info', text: ' droid-cua --platform ios --avd MySim (Force iOS platform)' });
77
75
  addOutput({ type: 'info', text: ' droid-cua --instructions tests/login.dcua --context app/context.md' });
78
- addOutput({ type: 'info', text: ' droid-cua --target web --url docs.example.com --instructions tests/search.dcua' });
76
+ addOutput({ type: 'info', text: ' droid-cua --target web --instructions tests/search.dcua' });
79
77
  addOutput({ type: 'info', text: ' /create login-test (design a new test)' });
80
78
  addOutput({ type: 'info', text: ' /list (see all tests)' });
81
79
  addOutput({ type: 'info', text: ' /view login-test (view test contents)' });
@@ -88,7 +88,7 @@ export async function handleRun(args, session, context) {
88
88
  });
89
89
  }
90
90
  // Set execution mode system prompt (replaces any design mode prompt)
91
- const executionPrompt = buildExecutionModePrompt(session.deviceInfo, {}, appContextText, { strictMode: Boolean(context.engine?.strictMode) });
91
+ const executionPrompt = buildExecutionModePrompt(session.deviceInfo, {}, appContextText);
92
92
  session.setSystemPrompt(executionPrompt);
93
93
  // Create execution mode
94
94
  const executionMode = new ExecutionMode(session, context.engine, instructions);
@@ -0,0 +1,5 @@
1
+ export const AIM_ASSIST_MODES = ["off", "smart", "always"];
2
+ export const DEFAULT_AIM_ASSIST_MODE = "smart";
3
+ export function normalizeAimAssistMode(value) {
4
+ return AIM_ASSIST_MODES.includes(value) ? value : DEFAULT_AIM_ASSIST_MODE;
5
+ }
@@ -101,55 +101,20 @@ function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
101
101
  }
102
102
  return content;
103
103
  }
104
- function getScopeAndIds(context = null, stepContext = null) {
105
- const scope = context?.sessionId ? "design" : "execution";
106
- const ids = scope === "design"
107
- ? {
108
- sessionId: context?.sessionId,
109
- stepId: stepContext?.stepId,
110
- instructionIndex: stepContext?.instructionIndex
111
- }
112
- : {
113
- runId: context?.runId,
114
- stepId: stepContext?.stepId,
115
- instructionIndex: stepContext?.instructionIndex
116
- };
117
- return { scope, ids };
118
- }
119
- function buildStrictModePlan(actions = [], strictMode = false) {
120
- if (!strictMode || actions.length === 0) {
121
- return {
122
- actionsToExecute: actions,
123
- droppedActions: [],
124
- truncationReason: null,
125
- runtimeNote: null
126
- };
127
- }
128
- const actionsToExecute = [actions[0]];
129
- const droppedActions = actions.slice(1);
130
- if (droppedActions.length === 0) {
131
- return {
132
- actionsToExecute,
133
- droppedActions,
134
- truncationReason: null,
135
- runtimeNote: null
136
- };
137
- }
138
- const leadingActionType = actionsToExecute[0]?.type;
139
- const truncationReason = leadingActionType === "screenshot"
140
- ? "leading_screenshot_reobserve"
141
- : "post_first_action_reobserve";
142
- const droppedActionTypes = droppedActions
143
- .map((action) => action?.type)
144
- .filter(Boolean);
145
- const runtimeNote = `Strict Mode: I executed only the first ${leadingActionType === "screenshot" ? "screenshot request" : "action"} from your previous chain and intentionally skipped the remaining ${droppedActions.length} action${droppedActions.length === 1 ? "" : "s"} (${droppedActionTypes.join(", ")}) so I could re-observe the device before continuing. Base your next step only on what is visible now.`;
104
+ function buildDebugContext(context = null, stepContext = null) {
146
105
  return {
147
- actionsToExecute,
148
- droppedActions,
149
- truncationReason,
150
- runtimeNote
106
+ scope: context?.sessionId ? "design" : "execution",
107
+ runId: context?.runId,
108
+ sessionId: context?.sessionId,
109
+ stepId: stepContext?.stepId,
110
+ instructionIndex: stepContext?.instructionIndex
151
111
  };
152
112
  }
113
+ function collectPendingSafetyChecks(items = []) {
114
+ return items
115
+ .filter((item) => item.type === "pending_safety_check")
116
+ .map((item) => ({ id: item.id }));
117
+ }
153
118
  function normalizeArtifactPaths(result) {
154
119
  if (result && typeof result === "object" && !Array.isArray(result)) {
155
120
  if (Array.isArray(result.annotationSourcePaths)) {
@@ -187,7 +152,6 @@ export class ExecutionEngine {
187
152
  this.recordScreenshots = options.recordScreenshots || false;
188
153
  this.screenshotDir = options.screenshotDir || null;
189
154
  this.screenshotRecorder = options.screenshotRecorder || null;
190
- this.strictMode = options.strictMode === true;
191
155
  this.stepDelayMs = options.stepDelayMs ?? getConfiguredStepDelayMs();
192
156
  this.reportedScreenshotWriteError = false;
193
157
  this.reportedScreenshotAnnotateError = false;
@@ -374,6 +338,108 @@ export class ExecutionEngine {
374
338
  }));
375
339
  }
376
340
  }
341
+ async requestNextTurn(messages, previousResponseId, context = null, stepContext = null) {
342
+ return await this.sendCUARequest({
343
+ messages,
344
+ previousResponseId,
345
+ deviceInfo: this.session.deviceInfo,
346
+ debugContext: buildDebugContext(context, stepContext)
347
+ });
348
+ }
349
+ async executeActionBatch(actions, shouldStop, context = null, stepContext = null, trackAction = null) {
350
+ let sawExplicitScreenshotAction = false;
351
+ for (const action of actions) {
352
+ const actionEventId = randomUUID();
353
+ if (shouldStop()) {
354
+ return { stopped: true, sawExplicitScreenshotAction };
355
+ }
356
+ if (action.type === "screenshot") {
357
+ sawExplicitScreenshotAction = true;
358
+ continue;
359
+ }
360
+ const annotatedScreenshotResult = await this.annotateLatestScreenshot(action, {
361
+ runId: context?.runId,
362
+ sessionId: context?.sessionId,
363
+ stepId: stepContext?.stepId,
364
+ instructionIndex: stepContext?.instructionIndex,
365
+ eventId: actionEventId,
366
+ captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
367
+ });
368
+ const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
369
+ const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
370
+ await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
371
+ ...context,
372
+ shouldStop,
373
+ stepId: stepContext?.stepId,
374
+ instructionIndex: stepContext?.instructionIndex,
375
+ eventId: actionEventId,
376
+ captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
377
+ ...(annotatedScreenshotPath ? { screenshotPath: annotatedScreenshotPath } : {}),
378
+ ...(context?.includeScreenshotDataUrl && annotatedScreenshotDataUrl ? { screenshotDataUrl: annotatedScreenshotDataUrl } : {})
379
+ });
380
+ if (trackAction) {
381
+ trackAction(action);
382
+ }
383
+ if (shouldStop()) {
384
+ return { stopped: true, sawExplicitScreenshotAction };
385
+ }
386
+ if (action.type !== "wait") {
387
+ await new Promise((resolve) => setTimeout(resolve, this.stepDelayMs));
388
+ }
389
+ }
390
+ return { stopped: false, sawExplicitScreenshotAction };
391
+ }
392
+ async buildActionFollowUp(callId, pendingSafetyChecks, sawExplicitScreenshotAction, addOutput, eventMeta, context = null, stepContext = null) {
393
+ const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
394
+ emitDesktopDebug("device.screenshot", "device", {
395
+ runId: context?.runId,
396
+ stepId: stepContext?.stepId,
397
+ instructionIndex: stepContext?.instructionIndex
398
+ }, {
399
+ source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
400
+ callId,
401
+ width: this.session.deviceInfo?.scaled_width,
402
+ height: this.session.deviceInfo?.scaled_height,
403
+ base64Length: screenshotBase64.length
404
+ });
405
+ const screenshotEventId = randomUUID();
406
+ const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
407
+ const screenshotResult = await this.recordScreenshot(screenshotBase64, {
408
+ runId: context?.runId,
409
+ sessionId: context?.sessionId,
410
+ stepId: stepContext?.stepId,
411
+ instructionIndex: stepContext?.instructionIndex,
412
+ eventId: screenshotEventId,
413
+ captureSource
414
+ });
415
+ const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
416
+ addOutput({
417
+ type: "info",
418
+ text: "Capturing screen",
419
+ eventType: "screenshot_captured",
420
+ actionType: "screenshot",
421
+ ...eventMeta({
422
+ payload: {
423
+ eventId: screenshotEventId,
424
+ captureSource,
425
+ source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
426
+ ...(typeof screenshotPath === "string" && screenshotPath ? { screenshotPath } : {}),
427
+ ...(context?.includeScreenshotDataUrl
428
+ ? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
429
+ : {})
430
+ }
431
+ })
432
+ });
433
+ return [{
434
+ type: "computer_call_output",
435
+ call_id: callId,
436
+ output: {
437
+ type: "computer_screenshot",
438
+ image_url: `data:image/png;base64,${screenshotBase64}`,
439
+ },
440
+ ...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
441
+ }];
442
+ }
377
443
  /**
378
444
  * Run a full turn with the CUA model
379
445
  * Executes actions until the model stops requesting actions
@@ -386,7 +452,6 @@ export class ExecutionEngine {
386
452
  const addOutput = context?.addOutput || printCliOutput;
387
453
  let newResponseId = response.id;
388
454
  const shouldStop = () => Boolean(trackAction?.());
389
- const { scope, ids } = getScopeAndIds(context, stepContext);
390
455
  const eventMeta = (extra = {}) => ({
391
456
  runId: context?.runId,
392
457
  stepId: stepContext?.stepId,
@@ -400,10 +465,7 @@ export class ExecutionEngine {
400
465
  }
401
466
  const items = response.output || [];
402
467
  const actionableItems = extractActionableOutputItems(items);
403
- // ── Collect pending safety checks ──
404
- const pendingSafetyChecks = items
405
- .filter(item => item.type === "pending_safety_check")
406
- .map(item => ({ id: item.id }));
468
+ const pendingSafetyChecks = collectPendingSafetyChecks(items);
407
469
  // ── Print assistant output ──
408
470
  for (const item of items) {
409
471
  if (item.type === "reasoning") {
@@ -461,154 +523,20 @@ export class ExecutionEngine {
461
523
  if (shouldStop()) {
462
524
  return newResponseId;
463
525
  }
464
- response = await this.sendCUARequest({
465
- messages: input,
466
- previousResponseId: newResponseId,
467
- deviceInfo: this.session.deviceInfo,
468
- debugContext: {
469
- scope,
470
- runId: context?.runId,
471
- sessionId: context?.sessionId,
472
- stepId: stepContext?.stepId,
473
- instructionIndex: stepContext?.instructionIndex
474
- }
475
- });
526
+ response = await this.requestNextTurn(input, newResponseId, context, stepContext);
476
527
  newResponseId = response.id;
477
528
  continue;
478
529
  }
479
530
  const { call_id, actions } = actionableItem;
480
- const { actionsToExecute, droppedActions, truncationReason, runtimeNote } = buildStrictModePlan(actions, this.strictMode);
481
- let sawExplicitScreenshotAction = false;
482
- for (const action of actionsToExecute) {
483
- const actionEventId = randomUUID();
484
- if (shouldStop()) {
485
- return newResponseId;
486
- }
487
- if (action.type === "screenshot") {
488
- sawExplicitScreenshotAction = true;
489
- }
490
- else {
491
- const annotatedScreenshotResult = await this.annotateLatestScreenshot(action, {
492
- runId: context?.runId,
493
- sessionId: context?.sessionId,
494
- stepId: stepContext?.stepId,
495
- instructionIndex: stepContext?.instructionIndex,
496
- eventId: actionEventId,
497
- captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
498
- });
499
- const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
500
- const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
501
- await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
502
- ...context,
503
- shouldStop,
504
- stepId: stepContext?.stepId,
505
- instructionIndex: stepContext?.instructionIndex,
506
- eventId: actionEventId,
507
- captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
508
- ...(annotatedScreenshotPath ? { screenshotPath: annotatedScreenshotPath } : {}),
509
- ...(context?.includeScreenshotDataUrl && annotatedScreenshotDataUrl ? { screenshotDataUrl: annotatedScreenshotDataUrl } : {})
510
- });
511
- if (trackAction) {
512
- trackAction(action);
513
- }
514
- if (shouldStop()) {
515
- return newResponseId;
516
- }
517
- if (action.type !== "wait") {
518
- await new Promise(resolve => setTimeout(resolve, this.stepDelayMs));
519
- }
520
- }
521
- }
522
- if (shouldStop()) {
531
+ const actionBatchResult = await this.executeActionBatch(actions, shouldStop, context, stepContext, trackAction);
532
+ if (actionBatchResult.stopped) {
523
533
  return newResponseId;
524
534
  }
525
- const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
526
- emitDesktopDebug("device.screenshot", "device", {
527
- runId: context?.runId,
528
- stepId: stepContext?.stepId,
529
- instructionIndex: stepContext?.instructionIndex
530
- }, {
531
- source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
532
- callId: call_id,
533
- width: this.session.deviceInfo?.scaled_width,
534
- height: this.session.deviceInfo?.scaled_height,
535
- base64Length: screenshotBase64.length
536
- });
537
- const screenshotEventId = randomUUID();
538
- const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
539
- const screenshotResult = await this.recordScreenshot(screenshotBase64, {
540
- runId: context?.runId,
541
- sessionId: context?.sessionId,
542
- stepId: stepContext?.stepId,
543
- instructionIndex: stepContext?.instructionIndex,
544
- eventId: screenshotEventId,
545
- captureSource
546
- });
547
- const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
548
- addOutput({
549
- type: 'info',
550
- text: 'Capturing screen',
551
- eventType: 'screenshot_captured',
552
- actionType: 'screenshot',
553
- ...eventMeta({
554
- payload: {
555
- eventId: screenshotEventId,
556
- captureSource,
557
- source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
558
- ...(typeof screenshotPath === "string" && screenshotPath ? { screenshotPath } : {}),
559
- ...(context?.includeScreenshotDataUrl
560
- ? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
561
- : {})
562
- }
563
- })
564
- });
565
- if (runtimeNote) {
566
- const executedActionTypes = actionsToExecute
567
- .map((action) => action?.type)
568
- .filter(Boolean);
569
- const droppedActionTypes = droppedActions
570
- .map((action) => action?.type)
571
- .filter(Boolean);
572
- emitDesktopDebug("cua.strict_mode.truncation", scope, ids, {
573
- callId: call_id,
574
- executedCount: actionsToExecute.length,
575
- droppedCount: droppedActions.length,
576
- executedActionTypes,
577
- droppedActionTypes,
578
- reason: truncationReason,
579
- note: runtimeNote
580
- });
581
- }
582
- const input = [{
583
- type: "computer_call_output",
584
- call_id,
585
- output: {
586
- type: "computer_screenshot",
587
- image_url: `data:image/png;base64,${screenshotBase64}`,
588
- },
589
- ...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
590
- }];
591
- if (runtimeNote) {
592
- input.push({
593
- role: "user",
594
- content: runtimeNote
595
- });
596
- }
535
+ const input = await this.buildActionFollowUp(call_id, pendingSafetyChecks, actionBatchResult.sawExplicitScreenshotAction, addOutput, eventMeta, context, stepContext);
597
536
  if (shouldStop()) {
598
537
  return newResponseId;
599
538
  }
600
- response = await this.sendCUARequest({
601
- messages: input,
602
- previousResponseId: newResponseId,
603
- deviceInfo: this.session.deviceInfo,
604
- debugContext: {
605
- scope,
606
- runId: context?.runId,
607
- sessionId: context?.sessionId,
608
- stepId: stepContext?.stepId,
609
- instructionIndex: stepContext?.instructionIndex
610
- }
611
- });
539
+ response = await this.requestNextTurn(input, newResponseId, context, stepContext);
612
540
  newResponseId = response.id;
613
541
  }
614
542
  // ── At end, if last output was only reasoning ──
@@ -35,10 +35,9 @@ export function resolveRuntimeCredentials({ openAiApiKey = null, loadmillApiToke
35
35
  loadmillBaseUrl: trimNullableString(loadmillBaseUrl),
36
36
  };
37
37
  }
38
- export function resolveRuntimeConfig({ cuaModel = null, strictMode = false, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
38
+ export function resolveRuntimeConfig({ cuaModel = null, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
39
39
  return {
40
40
  cuaModel: resolveCuaModel(cuaModel),
41
- strictMode: strictMode === true,
42
41
  stepDelayMs: normalizeStepDelayMs(Number(stepDelayMs)),
43
42
  contextOptimizationEnabled: normalizeContextOptimizationEnabled(contextOptimizationEnabled),
44
43
  contextOptimizationThreshold: normalizeContextOptimizationThreshold(contextOptimizationThreshold),
@@ -1,5 +1,6 @@
1
1
  import { buildTestRevisionSystemPrompt } from "../prompts/editor.js";
2
2
  import { createOpenAiClient, resolveRuntimeConfig } from "../core/runtime-config.js";
3
+ import { DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
3
4
  import { DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD } from "../core/context-optimization.js";
4
5
  import { logger } from "../utils/logger.js";
5
6
  import { CuaDebugTracer } from "../utils/cua-debug-tracer.js";
@@ -90,8 +91,10 @@ function buildContextManagement(contextOptimizationEnabled, contextOptimizationT
90
91
  }
91
92
  return [{ type: "compaction", compact_threshold: contextOptimizationThreshold }];
92
93
  }
93
- export function buildCuaRequestParams({ cuaModel, previousResponseId, input, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, }) {
94
+ export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, previousResponseId, input, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, }) {
94
95
  const contextManagement = buildContextManagement(contextOptimizationEnabled, contextOptimizationThreshold);
96
+ // Keep Responses API storage enabled on every turn so execution can chain
97
+ // reliably through previous_response_id across the whole run.
95
98
  return {
96
99
  model: cuaModel,
97
100
  reasoning: { effort: "medium" },
@@ -100,10 +103,12 @@ export function buildCuaRequestParams({ cuaModel, previousResponseId, input, con
100
103
  input,
101
104
  store: true,
102
105
  truncation: "auto",
103
- tools: [
104
- { type: "computer" },
105
- previewClickTool,
106
- ],
106
+ tools: aimAssistMode === "off"
107
+ ? [{ type: "computer" }]
108
+ : [
109
+ { type: "computer" },
110
+ previewClickTool,
111
+ ],
107
112
  };
108
113
  }
109
114
  /**
@@ -139,10 +144,11 @@ export async function reviseTestScript({ client, originalScript, revisionRequest
139
144
  * debugContext?: unknown,
140
145
  * }} options
141
146
  */
142
- export async function sendCUARequest({ client, cuaModel, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
147
+ export async function sendCUARequest({ client, cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
143
148
  const input = buildCuaInput({ messages, screenshotBase64, previousResponseId, callId });
144
149
  const requestParams = buildCuaRequestParams({
145
150
  cuaModel,
151
+ aimAssistMode,
146
152
  previousResponseId,
147
153
  input,
148
154
  contextOptimizationEnabled,
@@ -32,9 +32,7 @@ export class DesignModeInk {
32
32
  async start() {
33
33
  const addOutput = this.context.addOutput;
34
34
  // Set design mode system prompt
35
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, {
36
- strictMode: Boolean(this.engine?.strictMode)
37
- });
35
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
38
36
  this.baseDesignPrompt = designPrompt;
39
37
  this.session.setSystemPrompt(designPrompt);
40
38
  // Update UI
@@ -29,9 +29,7 @@ export class DesignMode {
29
29
  */
30
30
  async start(context) {
31
31
  // Set design mode system prompt
32
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, {
33
- strictMode: Boolean(this.engine?.strictMode)
34
- });
32
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
35
33
  this.baseDesignPrompt = designPrompt;
36
34
  this.session.setSystemPrompt(designPrompt);
37
35
  console.log(`\n=== Design Mode: Creating test "${this.testName}" ===`);
@@ -27,16 +27,6 @@ function appendCustomSections(prompt, sections = []) {
27
27
  ${customSection}
28
28
  `;
29
29
  }
30
- function buildStrictModeRuntimeSection(runtimeOptions = {}) {
31
- if (runtimeOptions?.strictMode !== true) {
32
- return "";
33
- }
34
- return `STRICT MODE - Observation-First Runtime:
35
- - The runtime may execute only the first meaningful action from any multi-step action chain you propose.
36
- - After that first action, it will re-observe the ${runtimeOptions?.platform === "browser" ? "browser" : "device"} and continue from a fresh screenshot.
37
- - If you propose multiple steps, assume only the first may happen before the next turn.
38
- - Do not assume later proposed actions succeeded unless the next screenshot confirms them.`;
39
- }
40
30
  function isBrowserTarget(deviceInfo = {}) {
41
31
  return typeof deviceInfo.platform === "string" && deviceInfo.platform.trim().toLowerCase() === "browser";
42
32
  }
@@ -55,7 +45,42 @@ function describeControlledTarget(deviceInfo = {}) {
55
45
  }
56
46
  return "a mobile device";
57
47
  }
58
- function buildBrowserBaseSystemPrompt(deviceInfo) {
48
+ function buildAlwaysAimAssistGuidance(targetKind) {
49
+ return [
50
+ "- Before every click action, call `preview_click` with the exact coordinates you intend to click.",
51
+ "- Review the returned annotated screenshot before clicking.",
52
+ `- Click if the marker is clearly inside the intended ${targetKind === "browser" ? "clickable" : "tappable"} control.${targetKind === "browser" ? "" : " Text inside a button, field, row, or bottom bar is okay."}`,
53
+ `- Adjust and preview again only if the marker is outside the target, on the ${targetKind === "browser" ? "edge" : "target border/edge"}, or closer to a neighboring target than the intended one.`,
54
+ "- If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.",
55
+ "- If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.",
56
+ "- Click precisely based on your visual estimate.",
57
+ ];
58
+ }
59
+ function buildAimAssistGuidance(targetKind, aimAssistMode = "always") {
60
+ if (aimAssistMode === "off") {
61
+ return [
62
+ "- Click directly based on your best visual estimate.",
63
+ "- If a click has no effect, treat it as a likely misclick and try a meaningfully different coordinate.",
64
+ "- Click precisely based on your visual estimate.",
65
+ ].join("\n ");
66
+ }
67
+ if (aimAssistMode === "smart") {
68
+ const targetWord = targetKind === "browser" ? "clickable" : "tappable";
69
+ return [
70
+ ...buildAlwaysAimAssistGuidance(targetKind),
71
+ "- In Smart mode, you may skip preview only when the target is broad, isolated, stable, and unambiguous, and you are confident the click point is clearly inside the intended target.",
72
+ "- Good skip examples are large empty text fields, large search fields, large CTA buttons with generous padding, launcher icons, and wide rows with generous spacing and no nearby competing actions.",
73
+ `- Do not skip preview when the target is small, dense, visually ambiguous, close to neighboring controls, or you are not confident the marker will land clearly inside the intended ${targetWord} target.`,
74
+ "- If a dropdown, expanded option list, popover, or select menu is visible, always use preview before selecting an option.",
75
+ "- Do not skip preview in search results, bottom sheets, bottom cards, modals, overlays, keyboard-driven flows, or after recent screen changes.",
76
+ "- Do not skip preview for known tricky UI such as dense lists, date pickers, time pickers, calendar cells, wheels, segmented selectors, and small inline controls inside larger rows or cards.",
77
+ "- If the target is one option among many visually similar options, use preview before clicking.",
78
+ ].join("\n ");
79
+ }
80
+ return buildAlwaysAimAssistGuidance(targetKind).join("\n ");
81
+ }
82
+ function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
83
+ const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
59
84
  return `
60
85
  You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
61
86
  Follow the user's instructions to interact with the browser.
@@ -67,13 +92,7 @@ function buildBrowserBaseSystemPrompt(deviceInfo) {
67
92
 
68
93
  When aiming for visual targets:
69
94
  - Reason carefully about the approximate pixel position.
70
- - Before every click action, call \`preview_click\` with the exact coordinates you intend to click.
71
- - Review the returned annotated screenshot before clicking.
72
- - Click if the marker is clearly inside the intended clickable target.
73
- - Adjust and preview again only if the marker is outside the target, on the edge, or closer to a neighboring target than the intended one.
74
- - If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.
75
- - If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.
76
- - Click precisely based on your visual estimate.
95
+ ${buildAimAssistGuidance("browser", aimAssistMode)}
77
96
 
78
97
  Available actions: click, scroll, type, keypress, wait, screenshot.
79
98
 
@@ -129,7 +148,8 @@ function buildBrowserBaseSystemPrompt(deviceInfo) {
129
148
  - CTRL/CMD/ALT/OPTION/SHIFT may be used when they are the most reliable path
130
149
  `;
131
150
  }
132
- function buildMobileBaseSystemPrompt(deviceInfo) {
151
+ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
152
+ const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
133
153
  return `
134
154
  You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
135
155
  Follow the user's instructions to interact with the device.
@@ -142,13 +162,7 @@ function buildMobileBaseSystemPrompt(deviceInfo) {
142
162
 
143
163
  When aiming for visual targets:
144
164
  - Reason carefully about the approximate pixel position.
145
- - Before every click action, call \`preview_click\` with the exact coordinates you intend to click.
146
- - Review the returned annotated screenshot before clicking.
147
- - Click if the marker is clearly inside the intended tappable control. Text inside a button, field, row, or bottom bar is okay.
148
- - Adjust and preview again only if the marker is outside the target, on the target border/edge, or closer to a neighboring target than the intended one.
149
- - If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.
150
- - If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.
151
- - Click precisely based on your visual estimate.
165
+ ${buildAimAssistGuidance("mobile", aimAssistMode)}
152
166
 
153
167
  Available actions: click, scroll, type, keypress, wait, screenshot.
154
168
 
@@ -209,9 +223,9 @@ function buildMobileBaseSystemPrompt(deviceInfo) {
209
223
  - Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
210
224
  `;
211
225
  }
212
- function buildBaseSystemPrompt(deviceInfo) {
226
+ function buildBaseSystemPrompt(deviceInfo, options = {}) {
213
227
  return isBrowserTarget(deviceInfo)
214
- ? buildBrowserBaseSystemPrompt(deviceInfo)
215
- : buildMobileBaseSystemPrompt(deviceInfo);
228
+ ? buildBrowserBaseSystemPrompt(deviceInfo, options)
229
+ : buildMobileBaseSystemPrompt(deviceInfo, options);
216
230
  }
217
- export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, buildStrictModeRuntimeSection, };
231
+ export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, };
@@ -1,8 +1,7 @@
1
- import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection, isBrowserTarget } from "./base.js";
2
- export function buildDesignModePrompt(deviceInfo, customInstructions = {}, runtimeOptions = {}) {
1
+ import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
2
+ export function buildDesignModePrompt(deviceInfo, customInstructions = {}) {
3
3
  const designCustomText = typeof customInstructions.designModeInstructions === "string" ? customInstructions.designModeInstructions.trim() : "";
4
4
  const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
5
- const strictModeSection = buildStrictModeRuntimeSection(runtimeOptions);
6
5
  const browserTarget = isBrowserTarget(deviceInfo);
7
6
  const prompt = `${basePrompt}
8
7
 
@@ -87,7 +86,7 @@ WRONG Example (DON'T DO THIS):
87
86
  \`\`\`
88
87
 
89
88
  Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.
90
- ` + (strictModeSection ? `\n\n${strictModeSection}` : "");
89
+ `;
91
90
  return appendCustomSections(prompt, [
92
91
  { title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
93
92
  { title: "Design Mode Instructions", text: designCustomText }