@loadmill/droid-cua 2.7.0 → 2.9.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.
Files changed (50) hide show
  1. package/README.md +7 -4
  2. package/build/index.js +131 -74
  3. package/build/src/browser/actions.js +9 -0
  4. package/build/src/browser/connection.js +37 -0
  5. package/build/src/browser/factory.js +22 -0
  6. package/build/src/browser/interface.js +14 -0
  7. package/build/src/browser/navigation.js +11 -0
  8. package/build/src/browser/normalize.js +36 -0
  9. package/build/src/browser/playwright/actions.js +199 -0
  10. package/build/src/browser/playwright/connection.js +207 -0
  11. package/build/src/browser/playwright/installed-browser.js +131 -0
  12. package/build/src/browser/playwright/session-state.js +19 -0
  13. package/build/src/browser/playwright/url.js +79 -0
  14. package/build/src/cli/headless-execution-config.js +4 -36
  15. package/build/src/cli/headless-target.js +64 -0
  16. package/build/src/cli/ink-shell.js +5 -2
  17. package/build/src/commands/help.js +9 -4
  18. package/build/src/commands/loadmill.js +10 -1
  19. package/build/src/commands/run.js +17 -28
  20. package/build/src/core/app-context.js +57 -33
  21. package/build/src/core/capture-sources.js +76 -0
  22. package/build/src/core/context-optimization.js +1 -1
  23. package/build/src/core/cua-models.js +9 -0
  24. package/build/src/core/execution-engine.js +43 -19
  25. package/build/src/core/prompts.js +1 -1
  26. package/build/src/core/runtime-config.js +96 -0
  27. package/build/src/core/session.js +18 -4
  28. package/build/src/device/action-event-meta.js +18 -0
  29. package/build/src/device/android/actions.js +6 -13
  30. package/build/src/device/cloud/actions.js +9 -14
  31. package/build/src/device/cloud/connection.js +8 -0
  32. package/build/src/device/ios/actions.js +6 -13
  33. package/build/src/device/ios/connection.js +8 -0
  34. package/build/src/device/loadmill.js +15 -9
  35. package/build/src/device/openai.js +67 -139
  36. package/build/src/integrations/loadmill/client.js +149 -131
  37. package/build/src/integrations/loadmill/executor.js +17 -11
  38. package/build/src/integrations/loadmill/index.js +1 -1
  39. package/build/src/integrations/loadmill/interpreter.js +4 -14
  40. package/build/src/modes/design-mode-ink.js +4 -3
  41. package/build/src/modes/design-mode.js +4 -3
  42. package/build/src/modes/execution-mode.js +139 -21
  43. package/build/src/prompts/base.js +92 -20
  44. package/build/src/prompts/design.js +6 -11
  45. package/build/src/prompts/execution.js +33 -109
  46. package/build/src/utils/console-output.js +8 -0
  47. package/build/src/utils/cua-debug-tracer.js +0 -1
  48. package/build/src/utils/run-screenshot-recorder.js +1 -1
  49. package/build/src/utils/structured-debug-log-manager.js +4 -1
  50. package/package.json +2 -1
@@ -1,7 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { CAPTURE_SOURCES } from "../core/capture-sources.js";
2
3
  import { getScreenshotAsBase64, connectToDevice, getDeviceInfo, getCurrentPlatform } from "../device/connection.js";
4
+ import { connectToBrowser, getBrowserInfo, getCurrentPlatform as getBrowserCurrentPlatform, navigateToUrl as navigateBrowserToUrl, } from "../browser/connection.js";
5
+ import { normalizeBrowserInfoForSession } from "../browser/normalize.js";
6
+ import { extractNavigateTarget, isNavigateInstruction } from "../browser/navigation.js";
3
7
  import { sendCUARequest } from "../device/openai.js";
4
- import { buildExecutionRecoveryPrompt } from "../core/prompts.js";
8
+ import { buildExecutionNavigationRecoveryPrompt, buildExecutionRecoveryPrompt } from "../core/prompts.js";
5
9
  import { isAssertion, extractAssertionPrompt, buildAssertionSystemPrompt, checkAssertionResult, handleAssertionFailure, handleAssertionSuccess, } from "../device/assertions.js";
6
10
  import { isLoadmillInstruction, extractLoadmillCommand, executeLoadmillInstruction, } from "../device/loadmill.js";
7
11
  import { logger } from "../utils/logger.js";
@@ -52,6 +56,16 @@ export class ExecutionMode {
52
56
  assertionsPassed: 0,
53
57
  assertionsFailed: 0,
54
58
  };
59
+ this.isBrowserSession = session?.targetKind === "browser" || session?.deviceInfo?.platform === "browser";
60
+ this.getScreenshotAsBase64 = executionEngine.getScreenshotAsBase64 || getScreenshotAsBase64;
61
+ this.sendCUARequest = executionEngine.sendCUARequest || sendCUARequest;
62
+ this.connectToDevice = executionEngine.connectToDevice || connectToDevice;
63
+ this.getDeviceInfo = executionEngine.getDeviceInfo || getDeviceInfo;
64
+ this.getCurrentPlatform =
65
+ executionEngine.getCurrentPlatform || (this.isBrowserSession ? getBrowserCurrentPlatform : getCurrentPlatform);
66
+ this.connectToBrowser = executionEngine.connectToBrowser || connectToBrowser;
67
+ this.getBrowserInfo = executionEngine.getBrowserInfo || getBrowserInfo;
68
+ this.navigateToUrl = executionEngine.navigateToUrl || navigateBrowserToUrl;
55
69
  }
56
70
  /**
57
71
  * Format duration in human-readable format (Xm Ys)
@@ -200,6 +214,94 @@ export class ExecutionMode {
200
214
  async executeInstruction(instruction, context, retryCount = 0, stepContext = null) {
201
215
  const MAX_RETRIES = 10;
202
216
  const addOutput = context.addOutput || printCliOutput;
217
+ if (isNavigateInstruction(instruction)) {
218
+ if (!this.isBrowserSession) {
219
+ const message = "navigate: instructions are only supported for web runs.";
220
+ this.emit(addOutput, "error", message, context, stepContext, {
221
+ eventType: "error",
222
+ payload: {
223
+ instructionType: "navigate",
224
+ message
225
+ }
226
+ });
227
+ return { success: false, error: message };
228
+ }
229
+ const navigationTarget = extractNavigateTarget(instruction);
230
+ this.session.addToTranscript(`[Navigate] ${navigationTarget}`);
231
+ emitDesktopDebug("runner.navigate.started", "execution", {
232
+ runId: context?.runId,
233
+ stepId: stepContext?.stepId,
234
+ instructionIndex: stepContext?.instructionIndex
235
+ }, {
236
+ instruction,
237
+ target: navigationTarget
238
+ });
239
+ this.emit(addOutput, "system", `Navigating to ${navigationTarget}...`, context, stepContext, {
240
+ eventType: "system_message",
241
+ payload: {
242
+ instructionType: "navigate",
243
+ target: navigationTarget,
244
+ }
245
+ });
246
+ try {
247
+ const normalizedUrl = await this.navigateToUrl(this.session.browserSessionId, navigationTarget);
248
+ const browserInfo = normalizeBrowserInfoForSession(await this.getBrowserInfo(this.session.browserSessionId));
249
+ this.session.browserInfo = browserInfo;
250
+ this.session.deviceInfo = browserInfo;
251
+ this.session.currentUrl = browserInfo.currentUrl || normalizedUrl;
252
+ this.session.browserName = browserInfo.browserName || this.session.currentUrl;
253
+ this.session.deviceName = this.session.browserName;
254
+ const transcriptContext = this.session.getTranscriptText();
255
+ const navigationRecoveryPrompt = buildExecutionNavigationRecoveryPrompt({
256
+ basePrompt: this.initialSystemText,
257
+ transcript: transcriptContext,
258
+ navigatedUrl: this.session.currentUrl,
259
+ });
260
+ this.session.installRecoverySystemPrompt(navigationRecoveryPrompt);
261
+ emitDesktopDebug("runner.navigate.completed", "execution", {
262
+ runId: context?.runId,
263
+ stepId: stepContext?.stepId,
264
+ instructionIndex: stepContext?.instructionIndex
265
+ }, {
266
+ instruction,
267
+ target: navigationTarget,
268
+ url: normalizedUrl
269
+ });
270
+ this.emit(addOutput, "success", `Navigation complete: ${normalizedUrl}`, context, stepContext, {
271
+ eventType: "system_message",
272
+ payload: {
273
+ instructionType: "navigate",
274
+ url: normalizedUrl,
275
+ }
276
+ });
277
+ return { success: true };
278
+ }
279
+ catch (err) {
280
+ const message = err instanceof Error ? err.message : "Navigation failed.";
281
+ logger.error("Execution navigation error", {
282
+ instruction,
283
+ retryCount,
284
+ message,
285
+ });
286
+ emitDesktopDebug("runner.navigate.failed", "execution", {
287
+ runId: context?.runId,
288
+ stepId: stepContext?.stepId,
289
+ instructionIndex: stepContext?.instructionIndex
290
+ }, {
291
+ instruction,
292
+ target: navigationTarget,
293
+ message
294
+ });
295
+ this.emit(addOutput, "error", `Navigation failed: ${message}`, context, stepContext, {
296
+ eventType: "error",
297
+ payload: {
298
+ instructionType: "navigate",
299
+ message,
300
+ }
301
+ });
302
+ return { success: false, error: message };
303
+ }
304
+ }
203
305
  // ── Check for Loadmill instruction ──
204
306
  if (isLoadmillInstruction(instruction)) {
205
307
  const loadmillCommand = extractLoadmillCommand(instruction);
@@ -230,13 +332,13 @@ export class ExecutionMode {
230
332
  this.session.addMessage("user", instruction);
231
333
  }
232
334
  try {
233
- const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
335
+ const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
234
336
  const screenshotResult = await this.engine.recordScreenshot?.(screenshotBase64, {
235
337
  runId: context?.runId,
236
338
  stepId: stepContext?.stepId,
237
339
  instructionIndex: stepContext?.instructionIndex,
238
340
  ...(assertionEventId ? { eventId: assertionEventId } : {}),
239
- captureSource: isAssertionStep ? "instruction-input-assertion" : "instruction-input"
341
+ captureSource: isAssertionStep ? CAPTURE_SOURCES.INSTRUCTION_INPUT_ASSERTION : CAPTURE_SOURCES.INSTRUCTION_INPUT
240
342
  });
241
343
  const assertionScreenshotPath = isAssertionStep ? screenshotPathFromRecorderResult(screenshotResult) : null;
242
344
  const assertionScreenshotDataUrl = isAssertionStep
@@ -259,7 +361,7 @@ export class ExecutionMode {
259
361
  messages: this.session.messages,
260
362
  previousResponseId: this.session.previousResponseId
261
363
  });
262
- const response = await sendCUARequest({
364
+ const response = await this.sendCUARequest({
263
365
  messages: messagesToSend,
264
366
  screenshotBase64,
265
367
  previousResponseId: previousResponseIdToSend,
@@ -364,35 +466,52 @@ export class ExecutionMode {
364
466
  attemptsExhausted: retryCount,
365
467
  maxRetries: MAX_RETRIES
366
468
  });
367
- this.emit(addOutput, 'error', `Failed after ${MAX_RETRIES} retries. Device may be disconnected.`, context, stepContext, {
469
+ this.emit(addOutput, 'error', `Failed after ${MAX_RETRIES} retries. ${this.isBrowserSession ? "Browser session" : "Device"} may be disconnected.`, context, stepContext, {
368
470
  eventType: 'error',
369
471
  payload: {
370
- message: `Failed after ${MAX_RETRIES} retries. Device may be disconnected.`,
472
+ message: `Failed after ${MAX_RETRIES} retries. ${this.isBrowserSession ? "Browser session" : "Device"} may be disconnected.`,
371
473
  attempt: retryCount,
372
474
  maxRetries: MAX_RETRIES
373
475
  }
374
476
  });
375
- // Attempt to reconnect to the device
376
- this.emit(addOutput, 'info', 'Attempting to reconnect to device...', context, stepContext, {
477
+ // Attempt to reconnect to the active target.
478
+ this.emit(addOutput, 'info', `Attempting to reconnect to ${this.isBrowserSession ? "browser" : "device"}...`, context, stepContext, {
377
479
  eventType: 'system_message'
378
480
  });
379
481
  try {
380
- const platform = getCurrentPlatform();
381
- const deviceName = this.session.deviceName || undefined;
382
- const deviceId = await connectToDevice(deviceName, platform);
383
- const deviceInfo = await getDeviceInfo(deviceId);
384
- // Update session with new connection
385
- this.session.deviceId = deviceId;
386
- this.session.deviceInfo = deviceInfo;
482
+ if (this.isBrowserSession) {
483
+ const browserSessionId = await this.connectToBrowser({
484
+ url: this.session.currentUrl || undefined,
485
+ headless: this.isHeadlessMode,
486
+ sessionMode: this.session.sessionMode || "persistent",
487
+ });
488
+ const browserInfo = normalizeBrowserInfoForSession(await this.getBrowserInfo(browserSessionId));
489
+ this.session.browserSessionId = browserSessionId;
490
+ this.session.deviceId = browserSessionId;
491
+ this.session.browserInfo = browserInfo;
492
+ this.session.deviceInfo = browserInfo;
493
+ this.session.currentUrl = browserInfo.currentUrl || null;
494
+ this.session.browserName = browserInfo.browserName || this.session.currentUrl;
495
+ this.session.deviceName = this.session.browserName;
496
+ }
497
+ else {
498
+ const platform = this.getCurrentPlatform();
499
+ const deviceName = this.session.deviceName || undefined;
500
+ const deviceId = await this.connectToDevice(deviceName, platform);
501
+ const deviceInfo = await this.getDeviceInfo(deviceId);
502
+ this.session.deviceId = deviceId;
503
+ this.session.deviceInfo = deviceInfo;
504
+ this.session.deviceName = deviceInfo?.device_name || this.session.deviceName;
505
+ }
387
506
  emitDesktopDebug("reconnect.attempt", "device", {
388
507
  runId: context?.runId,
389
508
  stepId: stepContext?.stepId,
390
509
  instructionIndex: stepContext?.instructionIndex
391
510
  }, {
392
511
  stage: "success",
393
- deviceId
512
+ deviceId: this.session.deviceId
394
513
  });
395
- this.emit(addOutput, 'success', 'Reconnected to device. Resuming...', context, stepContext, {
514
+ this.emit(addOutput, 'success', `Reconnected to ${this.isBrowserSession ? "browser" : "device"}. Resuming...`, context, stepContext, {
396
515
  eventType: 'system_message'
397
516
  });
398
517
  // Reset retry count and try again
@@ -408,13 +527,13 @@ export class ExecutionMode {
408
527
  message: reconnectErr.message
409
528
  });
410
529
  logger.error('Failed to reconnect to device', { error: reconnectErr.message });
411
- this.emit(addOutput, 'error', `Could not reconnect to device: ${reconnectErr.message}`, context, stepContext, {
530
+ this.emit(addOutput, 'error', `Could not reconnect to ${this.isBrowserSession ? "browser" : "device"}: ${reconnectErr.message}`, context, stepContext, {
412
531
  eventType: 'error',
413
532
  payload: {
414
533
  message: reconnectErr.message
415
534
  }
416
535
  });
417
- return { success: false, error: 'Device disconnected and reconnection failed' };
536
+ return { success: false, error: `${this.isBrowserSession ? "Browser" : "Device"} disconnected and reconnection failed` };
418
537
  }
419
538
  }
420
539
  this.emit(addOutput, 'info', 'Thinking...', context, stepContext, {
@@ -452,8 +571,7 @@ export class ExecutionMode {
452
571
  transcript: transcriptContext
453
572
  });
454
573
  // Replace the system message with the enhanced one
455
- this.session.messages = [{ role: "system", content: recoverySystemPrompt }];
456
- this.session.updateResponseId(undefined);
574
+ this.session.installRecoverySystemPrompt(recoverySystemPrompt);
457
575
  // Wait a bit before retrying
458
576
  await new Promise(resolve => setTimeout(resolve, 1000));
459
577
  // Retry the same instruction with incremented counter
@@ -33,13 +33,20 @@ function buildStrictModeRuntimeSection(runtimeOptions = {}) {
33
33
  }
34
34
  return `STRICT MODE - Observation-First Runtime:
35
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 device and continue from a fresh screenshot.
36
+ - After that first action, it will re-observe the ${runtimeOptions?.platform === "browser" ? "browser" : "device"} and continue from a fresh screenshot.
37
37
  - If you propose multiple steps, assume only the first may happen before the next turn.
38
38
  - Do not assume later proposed actions succeeded unless the next screenshot confirms them.`;
39
39
  }
40
- function describeControlledDevice(deviceInfo = {}) {
40
+ function isBrowserTarget(deviceInfo = {}) {
41
+ return typeof deviceInfo.platform === "string" && deviceInfo.platform.trim().toLowerCase() === "browser";
42
+ }
43
+ function describeControlledTarget(deviceInfo = {}) {
41
44
  const platform = typeof deviceInfo.platform === "string" ? deviceInfo.platform.trim().toLowerCase() : "";
42
45
  const deviceName = typeof deviceInfo.device_name === "string" ? deviceInfo.device_name.trim() : "";
46
+ const browserName = typeof deviceInfo.browserName === "string" ? deviceInfo.browserName.trim() : "";
47
+ if (platform === "browser") {
48
+ return browserName ? `a browser session (${browserName})` : "a browser session";
49
+ }
43
50
  if (platform === "ios") {
44
51
  return deviceName ? `an iOS simulator (${deviceName})` : "an iOS device";
45
52
  }
@@ -48,10 +55,83 @@ function describeControlledDevice(deviceInfo = {}) {
48
55
  }
49
56
  return "a mobile device";
50
57
  }
51
- function buildBaseSystemPrompt(deviceInfo, customInstructions = {}) {
52
- const controlledDevice = describeControlledDevice(deviceInfo);
53
- const prompt = `
54
- You are controlling ${controlledDevice} in a sandboxed testing environment.
58
+ function buildBrowserBaseSystemPrompt(deviceInfo) {
59
+ return `
60
+ You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
61
+ Follow the user's instructions to interact with the browser.
62
+
63
+ You can interact with any part of the visible browser window, including page content and browser chrome that is visible.
64
+
65
+ The screen you see is ${deviceInfo.scaled_width} x ${deviceInfo.scaled_height} pixels.
66
+ Pixel (0,0) is at the top-left corner.
67
+
68
+ When aiming for visual targets:
69
+ - 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.
77
+
78
+ Available actions: click, scroll, type, keypress, wait, screenshot.
79
+
80
+ CRITICAL - Desktop Browser Input Constraints:
81
+ - This is a desktop browser environment. Standard desktop keys and modifier chords are allowed when they are clearly useful.
82
+ - Prefer clicking visible controls over keyboard shortcuts when both are equally reliable.
83
+ - Use keypress chords only when they directly help the task, such as Enter to submit, Escape to dismiss a modal, or Control/Meta shortcuts for focused text fields.
84
+ - Do not rely on shortcuts that would switch away from the browser or operating system context.
85
+
86
+ CRITICAL - Automatic Timing:
87
+ - After EVERY action (click, type, keypress, scroll), there is an automatic 500ms delay
88
+ - This 500ms is sufficient for normal UI updates and animations
89
+ - DO NOT add 'wait' actions unnecessarily - trust the automatic delay
90
+
91
+ CRITICAL - Mutating Actions:
92
+ - Mutating actions change app state. Examples: submit, create, save, confirm, approve, reject, login, logout, send, place order, initiate transfer
93
+ - Before clicking a mutating action button, dismiss any open overlay or on-screen keyboard if it blocks the target and is not needed
94
+ - After performing a mutating action once, do NOT repeat the same mutating action unless the UI clearly shows the first attempt failed or had no effect
95
+ - Treat visible state change as success. Examples: form fields clear, submit button returns to normal, status changes, list refreshes, new row appears, success message appears, screen changes
96
+ - For form submissions specifically, if the relevant fields clear and the action button returns to its normal idle state, treat that as success even if the new row or confirmation is not obvious yet
97
+ - If the UI shows signs that the mutating action succeeded, stop acting for that instruction
98
+
99
+ Use explicit 'wait' action ONLY in these specific cases:
100
+ 1. After pressing ENTER that triggers navigation (search, URL, form submit)
101
+ 2. After clicking links or controls that trigger page navigation
102
+ 3. After actions that trigger heavy loading, a full-page refresh, or a visible URL change
103
+
104
+ Rule of thumb: Wait for page navigation and page loads. Everything else has automatic timing.
105
+
106
+ Perform the user's requested actions within the current view.
107
+
108
+ If unsure about visual elements, take a screenshot to improve your reasoning.
109
+ If unsure about the user's intent, make the best decision you can based on context and continue automatically.
110
+
111
+ CRITICAL - Never Ask Questions:
112
+ - NEVER ask the user for confirmation, clarification, or next steps
113
+ - NEVER ask questions like "Should I...", "Would you like...", "Do you want me to..."
114
+ - NEVER wait for user guidance - make autonomous decisions
115
+ - If stuck, try alternative approaches (go back, try different UI element, restart app)
116
+ - ONLY stop when the task is complete or you've exhausted reasonable approaches
117
+
118
+ Act decisively to complete the task.
119
+
120
+ Stop acting once the task appears complete.
121
+ Only complete the current instruction. Do not proceed beyond the current step unless asked.
122
+
123
+ Browser-Specific Notes:
124
+ - You are operating a desktop browser, not a phone UI
125
+ - Direct URL navigation is handled by runner-owned \`navigate:\` lines when available; do not try to reach the browser address bar or fake URL entry with keyboard shortcuts
126
+ - ESC dismisses many overlays and dialogs
127
+ - Cookie or consent banners may block the main task and may need dismissal first
128
+ - Vertical scrolling is the default way to discover content below the fold
129
+ - CTRL/CMD/ALT/OPTION/SHIFT may be used when they are the most reliable path
130
+ `;
131
+ }
132
+ function buildMobileBaseSystemPrompt(deviceInfo) {
133
+ return `
134
+ You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
55
135
  Follow the user's instructions to interact with the device.
56
136
 
57
137
  The device screen has been scaled down for display.
@@ -103,18 +183,6 @@ function buildBaseSystemPrompt(deviceInfo, customInstructions = {}) {
103
183
  3. After clicking links that open new apps or pages
104
184
  4. After actions that trigger heavy loading (camera, maps, etc.)
105
185
 
106
- When you MUST wait:
107
- - Click app icon from home → wait → Continue
108
- - Type in search box → Press ENTER → wait → Continue
109
- - Click link that opens new page/app → wait → Continue
110
- - Open camera/maps/heavy feature → wait → Continue
111
-
112
- When you should NOT wait (automatic 500ms handles it):
113
- - Clicking UI buttons within a running app (click button - no wait needed)
114
- - Typing in text fields (type text - no wait needed)
115
- - Scrolling (scroll - no wait needed)
116
- - Clicking tabs or menu items within an app (click - no wait needed)
117
-
118
186
  Rule of thumb: Wait for app launches and navigation. Everything else has automatic timing.
119
187
 
120
188
  Perform the user's requested actions within the current view.
@@ -140,6 +208,10 @@ function buildBaseSystemPrompt(deviceInfo, customInstructions = {}) {
140
208
  - On iOS, ESC has no effect; use visible on-screen controls instead
141
209
  - Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
142
210
  `;
143
- return prompt;
144
211
  }
145
- export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledDevice, buildStrictModeRuntimeSection, };
212
+ function buildBaseSystemPrompt(deviceInfo) {
213
+ return isBrowserTarget(deviceInfo)
214
+ ? buildBrowserBaseSystemPrompt(deviceInfo)
215
+ : buildMobileBaseSystemPrompt(deviceInfo);
216
+ }
217
+ export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, buildStrictModeRuntimeSection, };
@@ -1,17 +1,18 @@
1
- import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection } from "./base.js";
1
+ import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection, isBrowserTarget } from "./base.js";
2
2
  export function buildDesignModePrompt(deviceInfo, customInstructions = {}, runtimeOptions = {}) {
3
3
  const designCustomText = typeof customInstructions.designModeInstructions === "string" ? customInstructions.designModeInstructions.trim() : "";
4
4
  const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
5
5
  const strictModeSection = buildStrictModeRuntimeSection(runtimeOptions);
6
+ const browserTarget = isBrowserTarget(deviceInfo);
6
7
  const prompt = `${basePrompt}
7
8
 
8
9
  DESIGN MODE:
9
- You are helping design a test script for an Android app.
10
+ You are helping design a test script for ${browserTarget ? "a browser workflow" : "a mobile app"}.
10
11
  Some tests intentionally validate negative outcomes (errors, failures, rejected inputs). These are expected and should be treated as successful progress when they match the test goal.
11
12
 
12
13
  Your task:
13
14
  1. Understand what the user wants to test from their initial instruction
14
- 2. Explore the app autonomously to understand the flows
15
+ 2. Explore the ${browserTarget ? "website" : "app"} autonomously to understand the flows
15
16
  3. Take screenshots and interact as needed to discover the UI and behavior
16
17
  4. Once you've successfully completed the user's requested flow, immediately generate the test script
17
18
 
@@ -71,16 +72,10 @@ Suggested test name: short-kebab-case-name
71
72
  \`\`\`
72
73
 
73
74
  CORRECT Example:
74
- Suggested test name: calculator-addition
75
+ Suggested test name: ${browserTarget ? "docs-search" : "calculator-addition"}
75
76
 
76
77
  \`\`\`
77
- Open Calculator app
78
- assert: Calculator app is visible
79
- Type "2"
80
- Click the plus button
81
- Type "3"
82
- Click the equals button
83
- assert: result shows 5
78
+ ${browserTarget ? "navigate: https://docs.example.com\nType \"pricing\" in the search field\nPress Enter\nwait\nClick \"Pricing\"\nassert: Pricing page is visible" : "Open Calculator app\nassert: Calculator app is visible\nType \"2\"\nClick the plus button\nType \"3\"\nClick the equals button\nassert: result shows 5"}
84
79
  exit
85
80
  \`\`\`
86
81
 
@@ -1,20 +1,21 @@
1
- import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection } from "./base.js";
2
- export function buildAppContextSection(briefing) {
3
- const text = typeof briefing === "string" ? briefing.trim() : "";
4
- if (!text) {
1
+ import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection, isBrowserTarget } from "./base.js";
2
+ export function buildAppContextSection(text) {
3
+ const trimmed = typeof text === "string" ? text.trim() : "";
4
+ if (!trimmed) {
5
5
  return "";
6
6
  }
7
- return `APP CONTEXT BRIEFING:
8
- The following is a condensed description of the app you are testing, relevant to the current task.
7
+ return `APP CONTEXT:
8
+ The following describes the app you are testing.
9
9
  Use this to understand screen layouts, terminology, navigation, and expected behavior.
10
10
 
11
- ${text}`;
11
+ ${trimmed}`;
12
12
  }
13
- export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextBriefing = "", runtimeOptions = {}) {
13
+ export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextText = "", runtimeOptions = {}) {
14
14
  const executionCustomText = typeof customInstructions.executionModeInstructions === "string" ? customInstructions.executionModeInstructions.trim() : "";
15
15
  const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
16
- const appContextSection = buildAppContextSection(appContextBriefing);
16
+ const appContextSection = buildAppContextSection(appContextText);
17
17
  const strictModeSection = buildStrictModeRuntimeSection(runtimeOptions);
18
+ const browserTarget = isBrowserTarget(deviceInfo);
18
19
  const prompt = `${basePrompt}
19
20
 
20
21
  EXECUTION MODE - Critical Behavior:
@@ -27,12 +28,12 @@ CRITICAL RULES:
27
28
  - DO NOT say "Let me know if you need help" or similar phrases
28
29
  - Just execute the action silently and stop immediately
29
30
  - Only generate text if the action FAILED or cannot be completed
30
- - Never emit desktop keyboard shortcuts or modifier combos; mobile execution only supports mobile-safe single-key presses
31
+ - ${browserTarget ? "Use browser-appropriate keyboard input only when it is the most reliable path" : "Never emit desktop keyboard shortcuts or modifier combos; mobile execution only supports mobile-safe single-key presses"}
31
32
  - Never repeat the same mutating action with the same apparent intent unless the UI clearly shows failure or no state change
32
33
  - If a submit/create/approve/reject/login action appears to succeed, stop instead of trying to reconfirm by doing it again
33
34
  - For form submissions, cleared fields plus a reset action button are strong success signals; stop even if the created item is not yet obvious in the visible list
34
35
  - If target is not visible, perform bounded off-screen discovery first:
35
- 1. Scroll the screen in the likely direction to reveal hidden controls
36
+ 1. Scroll ${browserTarget ? "vertically" : "the screen"} in the likely direction to reveal hidden controls
36
37
  2. If still missing, do one minimal fallback (e.g., close overlay or go back once), then retry
37
38
 
38
39
  Your process:
@@ -61,6 +62,27 @@ ${transcript}
61
62
 
62
63
  [IMPORTANT: Resume execution silently. Do NOT narrate or explain. Just execute the next instruction.]`;
63
64
  }
65
+ export function buildExecutionNavigationRecoveryPrompt({ basePrompt, transcript, navigatedUrl }) {
66
+ const normalizedTranscript = typeof transcript === "string" ? transcript.trim() : "";
67
+ const normalizedUrl = typeof navigatedUrl === "string" ? navigatedUrl.trim() : "";
68
+ if (!normalizedTranscript && !normalizedUrl) {
69
+ return basePrompt;
70
+ }
71
+ const transitionLine = normalizedUrl
72
+ ? `[RUNNER NAVIGATION - The runtime navigated directly to ${normalizedUrl}. The page content has changed.]`
73
+ : "[RUNNER NAVIGATION - The runtime navigated directly. The page content has changed.]";
74
+ const transcriptSection = normalizedTranscript
75
+ ? `
76
+
77
+ [PRIOR TEST CONTEXT - Steps completed before this navigation:]
78
+ ${normalizedTranscript}`
79
+ : "";
80
+ return `${basePrompt}
81
+
82
+ ${transitionLine}${transcriptSection}
83
+
84
+ [IMPORTANT: Continue the current test from this new page. Use the prior context above, but rely on the latest screenshot for the current UI.]`;
85
+ }
64
86
  export function buildAssertionSystemPrompt(baseSystemPrompt, assertionPrompt) {
65
87
  return `${baseSystemPrompt}
66
88
 
@@ -82,101 +104,3 @@ Remember:
82
104
  - You must include either "ASSERTION RESULT: PASS" or "ASSERTION RESULT: FAIL" in your response.
83
105
  - Be thorough but efficient. Only take the actions necessary to validate the assertion.`;
84
106
  }
85
- export function buildAppContextCompactionInput({ contextDocument, taskDescription, tokenBudget }) {
86
- return [
87
- {
88
- role: "system",
89
- content: [{
90
- type: "input_text",
91
- text: `You are compressing an app context document for a mobile testing agent.
92
-
93
- You will receive:
94
- 1. A context document
95
- 2. A test task
96
-
97
- Your job is to SELECT only the facts from the context document that are useful for the given task.
98
- The output will be injected into a system prompt with a strict token budget.
99
-
100
- CRITICAL:
101
- - Use only facts explicitly supported by the context document
102
- - Never invent, infer, normalize, substitute, or improve credentials, labels, screen names, button names, or numeric values
103
- - Preserve exact values verbatim when present in the source
104
- - Prefer facts that help the agent act correctly when they are not obvious from the task alone
105
- - Do not restate, paraphrase, summarize, or reorganize the test task
106
- - The output must not read like instructions or a test plan
107
- - Do not describe what the agent should do
108
- - Output only reference knowledge about the app
109
- - If a line could be copied from the task with minor wording changes, omit it
110
- - Prefer copying source facts verbatim or near-verbatim over rewriting them
111
- - Do not collapse multiple specific source facts into one generic summary if that removes useful distinctions
112
-
113
- Selection priority:
114
- 1. Facts the agent would NOT know from the test script alone
115
- 2. Facts that are hard to infer from screenshots
116
- 3. Non-obvious navigation or interaction details
117
- 4. Exact visible labels needed to act correctly
118
- 5. Credentials and other exact values
119
-
120
- High-value facts:
121
- - exact UI labels
122
- - how state, mode, or account selection is performed
123
- - where logout is located
124
- - hidden or non-obvious navigation
125
- - which menu items are decorative or non-functional
126
- - screen titles and section labels used to confirm location
127
- - exact credentials and role labels
128
-
129
- Low-value facts:
130
- - restating the test steps
131
- - repeating literal values already present in the task
132
- - generic summaries like "approve the transaction"
133
-
134
- When the task involves authentication, switching state or mode, opening menus, or moving between major areas of the app, strongly prefer including:
135
- - how account, state, or mode selection is performed
136
- - exact visible labels for the relevant controls
137
- - where exit or sign-out actions are located
138
- - the screen or section labels that confirm the agent is in the right place
139
-
140
- Rules:
141
- - Output plain text only
142
- - No markdown, no bullet symbols, no numbering, no headers
143
- - Use terse, factual language: one fact per line, no filler words
144
- - Blank lines only to separate logical groups
145
- - Prefer exact visible UI labels over summaries
146
- - Do not describe step-by-step procedures
147
- - Do not restate the test workflow
148
- - State only facts about screens, elements, hidden interactions, entities, credentials, and navigation
149
- - If a useful fact is not explicitly stated in the context document, omit it
150
- - Include only information relevant to this task
151
- - Do not waste space repeating the task itself
152
- - If the task already states a value or action, include it only when the context adds non-obvious execution details
153
- - Return a short result or an empty string if little is relevant
154
- - Target: under ${tokenBudget} tokens
155
-
156
- Bad output patterns to avoid:
157
- - generic summaries that remove actionable details
158
- - lines that restate the task in generic prose
159
- - lines that describe obvious workflow steps instead of app knowledge
160
- - lines that replace exact source labels or mechanisms with broad summaries
161
-
162
- Good output characteristics:
163
- - preserves the exact label or mechanism from the source when it matters
164
- - keeps distinctions like dropdown vs tabs, drawer vs visible button, exact section titles, exact button text
165
- - includes hidden or non-obvious navigation details when relevant
166
-
167
- Return only the briefing text.`
168
- }]
169
- },
170
- {
171
- role: "user",
172
- content: [{
173
- type: "input_text",
174
- text: `APP CONTEXT DOCUMENT:
175
- ${contextDocument}
176
-
177
- TASK:
178
- ${taskDescription}`
179
- }]
180
- }
181
- ];
182
- }
@@ -1,3 +1,8 @@
1
+ import { classifyCaptureSource } from "../core/capture-sources.js";
2
+ function isHiddenExecutionOutput(item) {
3
+ const captureSource = item?.payload?.captureSource;
4
+ return classifyCaptureSource(captureSource).mediaKind === "hidden-debug";
5
+ }
1
6
  export function formatCliOutput(item) {
2
7
  if (item == null) {
3
8
  return null;
@@ -8,6 +13,9 @@ export function formatCliOutput(item) {
8
13
  if (typeof item !== "object") {
9
14
  return String(item);
10
15
  }
16
+ if (isHiddenExecutionOutput(item)) {
17
+ return null;
18
+ }
11
19
  const text = typeof item.text === "string" ? item.text : null;
12
20
  const type = typeof item.type === "string" ? item.type : "";
13
21
  const eventType = typeof item.eventType === "string" ? item.eventType : "";
@@ -94,7 +94,6 @@ function redactRequestForFileLog(requestParams) {
94
94
  ...item.output,
95
95
  image_url: `data:image/png;base64,[${base64Data.length} chars]`
96
96
  },
97
- current_url: item.current_url,
98
97
  acknowledged_safety_checks: item.acknowledged_safety_checks
99
98
  };
100
99
  }
@@ -126,7 +126,7 @@ async function renderAnnotatedScreenshot(image, marker = {}) {
126
126
  return null;
127
127
  }
128
128
  const placement = buildMarkerPlacement(marker, width, height);
129
- const radius = Math.max(14, Math.round(Math.min(width, height) * 0.055));
129
+ const radius = Math.max(16, Math.min(22, Math.round(Math.min(width, height) * 0.05)));
130
130
  const overlay = Buffer.from(buildClickMarkerSvg({ width, height, x: placement.x, y: placement.y, radius, marker }));
131
131
  const outputBuffer = await image
132
132
  .composite([{ input: overlay }])
@@ -20,7 +20,10 @@ function compactTimestamp(iso) {
20
20
  }
21
21
  function sanitizeValue(value, pathParts = [], eventName = "") {
22
22
  if (typeof value === "string") {
23
- if (eventName === "cua.request.full" || eventName === "cua.response.full" || eventName === "app_context.briefing.full") {
23
+ if (eventName === "cua.request.full" ||
24
+ eventName === "cua.response.full" ||
25
+ eventName === "app_context.briefing.full" ||
26
+ eventName === "app_context.loaded") {
24
27
  return value;
25
28
  }
26
29
  if (isPromptContentPath(pathParts)) {