@loadmill/droid-cua 2.10.0 → 2.10.1-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +11 -0
- package/build/src/core/aim-assist.js +1 -1
- package/build/src/core/execution-engine.js +191 -358
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/screenshot-store.js +153 -0
- package/build/src/core/tool-handlers/assert.js +155 -0
- package/build/src/core/tool-handlers/loadmill.js +29 -0
- package/build/src/core/tool-handlers/navigate.js +110 -0
- package/build/src/core/tool-handlers/preview-click.js +112 -0
- package/build/src/core/tool-handlers/test-design-complete.js +93 -0
- package/build/src/core/tool-handlers/utils.js +80 -0
- package/build/src/device/openai.js +101 -8
- package/build/src/modes/design-mode-ink.js +16 -2
- package/build/src/modes/design-mode.js +16 -2
- package/build/src/modes/execution-mode.js +42 -234
- package/build/src/prompts/base.js +20 -21
- package/build/src/prompts/design.js +52 -11
- package/build/src/prompts/editor.js +1 -1
- package/build/src/prompts/execution.js +9 -5
- package/build/src/utils/cua-debug-tracer.js +3 -1
- package/package.json +3 -4
- package/build/src/device/screenshot-resolution.js +0 -33
|
@@ -45,17 +45,6 @@ function describeControlledTarget(deviceInfo = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
return "a mobile device";
|
|
47
47
|
}
|
|
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
48
|
function buildAimAssistGuidance(targetKind, aimAssistMode = "always") {
|
|
60
49
|
if (aimAssistMode === "off") {
|
|
61
50
|
return [
|
|
@@ -65,19 +54,29 @@ function buildAimAssistGuidance(targetKind, aimAssistMode = "always") {
|
|
|
65
54
|
].join("\n ");
|
|
66
55
|
}
|
|
67
56
|
if (aimAssistMode === "smart") {
|
|
68
|
-
const targetWord = targetKind === "browser" ? "clickable" : "tappable";
|
|
69
57
|
return [
|
|
70
|
-
|
|
71
|
-
"-
|
|
58
|
+
"- Before every click action, call `preview_click` with the exact coordinates you intend to click.",
|
|
59
|
+
"- Review the returned annotated screenshot before clicking.",
|
|
60
|
+
`- Click if the marker is clearly inside the intended ${targetKind === "browser" ? "clickable target" : "tappable control"}.${targetKind === "browser" ? "" : " Text inside a button, field, row, or bottom bar is okay."}`,
|
|
61
|
+
`- 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.`,
|
|
62
|
+
"- If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.",
|
|
63
|
+
"- If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.",
|
|
64
|
+
"- Click precisely based on your visual estimate.",
|
|
65
|
+
"- 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
66
|
"- 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
|
-
|
|
74
|
-
"-
|
|
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.",
|
|
67
|
+
"- Always use preview for dropdowns, expanded option lists, popovers, select menus, dense lists or search results, bottom sheets or cards, modals, overlays, date or time pickers, calendar cells, wheels, segmented selectors, and small inline controls.",
|
|
68
|
+
"- Do not skip preview when the target is one of many similar options, close to neighboring controls, in a keyboard-driven flow, after a recent screen change, or whenever you are not confident.",
|
|
78
69
|
].join("\n ");
|
|
79
70
|
}
|
|
80
|
-
return
|
|
71
|
+
return [
|
|
72
|
+
"- Before every click action, call `preview_click` with the exact coordinates you intend to click.",
|
|
73
|
+
"- Review the returned annotated screenshot before clicking.",
|
|
74
|
+
`- Click if the marker is clearly inside the intended ${targetKind === "browser" ? "clickable target" : "tappable control"}.${targetKind === "browser" ? "" : " Text inside a button, field, row, or bottom bar is okay."}`,
|
|
75
|
+
`- 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.`,
|
|
76
|
+
"- If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.",
|
|
77
|
+
"- If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.",
|
|
78
|
+
"- Click precisely based on your visual estimate.",
|
|
79
|
+
].join("\n ");
|
|
81
80
|
}
|
|
82
81
|
function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
|
|
83
82
|
const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
|
|
@@ -141,7 +140,7 @@ function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
141
140
|
|
|
142
141
|
Browser-Specific Notes:
|
|
143
142
|
- You are operating a desktop browser, not a phone UI
|
|
144
|
-
-
|
|
143
|
+
- Use the \`navigate\` tool for direct URL changes when available; do not try to reach the browser address bar or fake URL entry with keyboard shortcuts
|
|
145
144
|
- ESC dismisses many overlays and dialogs
|
|
146
145
|
- Cookie or consent banners may block the main task and may need dismissal first
|
|
147
146
|
- Vertical scrolling is the default way to discover content below the fold
|
|
@@ -13,15 +13,21 @@ Your task:
|
|
|
13
13
|
1. Understand what the user wants to test from their initial instruction
|
|
14
14
|
2. Explore the ${browserTarget ? "website" : "app"} autonomously to understand the flows
|
|
15
15
|
3. Take screenshots and interact as needed to discover the UI and behavior
|
|
16
|
-
4. Once you've successfully completed the user's requested flow,
|
|
16
|
+
4. Once you've successfully completed the user's requested flow, call \`test_design_complete\` with a structured summary of what was proven
|
|
17
17
|
|
|
18
18
|
CRITICAL - After Completing the Task:
|
|
19
19
|
- DO NOT navigate back or away from the final screen
|
|
20
20
|
- The final screen state is what matters for verification
|
|
21
|
-
-
|
|
21
|
+
- Call \`test_design_complete\` immediately once the requested flow has been proven
|
|
22
22
|
- Use assertions to verify state, not navigation
|
|
23
23
|
- "Check that it changed" means verify the current visual state, not navigate elsewhere
|
|
24
|
-
- If the target validation state is visible (including expected error states), STOP actions and
|
|
24
|
+
- If the target validation state is visible (including expected error states), STOP actions and call \`test_design_complete\`
|
|
25
|
+
- If an assertion or validation PASS confirms the user goal, treat the task as complete
|
|
26
|
+
- After a passing assertion confirms the goal, your very next action should be calling \`test_design_complete\`
|
|
27
|
+
- Do NOT repeat the same assertion or validation on an unchanged screen after it has already passed
|
|
28
|
+
- Do NOT keep browsing, screenshotting, or re-checking once the goal has been confirmed
|
|
29
|
+
- Do NOT output the final test script before calling \`test_design_complete\`
|
|
30
|
+
- After calling \`test_design_complete\`, stop exploring and wait for the final script-generation turn
|
|
25
31
|
|
|
26
32
|
CRITICAL - Recognizing When You Are Stuck:
|
|
27
33
|
If you find yourself:
|
|
@@ -39,7 +45,7 @@ Example:
|
|
|
39
45
|
"Chrome is open but I don't see a search bar or new tab button. Should I open a new tab, or is there a specific way you'd like me to navigate?"
|
|
40
46
|
|
|
41
47
|
DO NOT continue brute-forcing the UI when stuck. The user prefers being asked over watching repeated failed attempts.
|
|
42
|
-
DO NOT ask if the user wants a script after successfully completing the flow -
|
|
48
|
+
DO NOT ask if the user wants a script after successfully completing the flow - complete the handoff automatically.
|
|
43
49
|
|
|
44
50
|
CRITICAL - Off-Screen Element Discovery:
|
|
45
51
|
- If a required element is not visible, assume it may be off-screen before changing strategy
|
|
@@ -52,11 +58,11 @@ CRITICAL - Off-Screen Element Discovery:
|
|
|
52
58
|
CRITICAL - Test Script Format Rules:
|
|
53
59
|
- One simple instruction per line (NO numbers, NO bullets)
|
|
54
60
|
- Use imperative commands: "Open X", "Click Y", "Type Z"
|
|
55
|
-
-
|
|
56
|
-
- Normalize validation wording into
|
|
57
|
-
- Convert "check", "
|
|
58
|
-
- Do not leave standalone "Check ..."
|
|
59
|
-
- Merge duplicate or near-duplicate
|
|
61
|
+
- Use plain-language verification lines such as "Verify ..." or "Validate ..."
|
|
62
|
+
- Normalize validation wording into plain-language verification:
|
|
63
|
+
- Convert "check", "assert", "ensure", "fetch", and "compare" intent into clear "Verify ..." or "Validate ..." lines
|
|
64
|
+
- Do not leave vague standalone "Check ..." lines in the final script
|
|
65
|
+
- Merge duplicate or near-duplicate verification lines into one clear validation step
|
|
60
66
|
- End with "exit"
|
|
61
67
|
- Keep it simple and executable
|
|
62
68
|
- When you generate the final result, include a suggested test name before the script
|
|
@@ -74,7 +80,7 @@ CORRECT Example:
|
|
|
74
80
|
Suggested test name: ${browserTarget ? "docs-search" : "calculator-addition"}
|
|
75
81
|
|
|
76
82
|
\`\`\`
|
|
77
|
-
${browserTarget ? "
|
|
83
|
+
${browserTarget ? "Go to https://docs.example.com\nType \"pricing\" in the search field\nPress Enter\nwait\nClick \"Pricing\"\nVerify the Pricing page is visible" : "Open Calculator app\nVerify the Calculator app is visible\nType \"2\"\nClick the plus button\nType \"3\"\nClick the equals button\nVerify the result shows 5"}
|
|
78
84
|
exit
|
|
79
85
|
\`\`\`
|
|
80
86
|
|
|
@@ -105,5 +111,40 @@ ${transcript}
|
|
|
105
111
|
Original objective:
|
|
106
112
|
${objective ?? "(not provided)"}
|
|
107
113
|
|
|
108
|
-
If the objective is already completed,
|
|
114
|
+
If the objective is already completed, or a recent passing validation already confirmed the goal, call \`test_design_complete\` now.
|
|
115
|
+
Do not repeat the same assertion on an unchanged screen.`;
|
|
116
|
+
}
|
|
117
|
+
function formatStringList(items = []) {
|
|
118
|
+
const normalized = Array.isArray(items)
|
|
119
|
+
? items
|
|
120
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
: [];
|
|
123
|
+
if (normalized.length === 0) {
|
|
124
|
+
return "- (none provided)";
|
|
125
|
+
}
|
|
126
|
+
return normalized.map((item) => `- ${item}`).join("\n");
|
|
127
|
+
}
|
|
128
|
+
export function buildDesignScriptGenerationPrompt(summary = {}) {
|
|
129
|
+
const suggestedTestName = typeof summary.suggestedTestName === "string" ? summary.suggestedTestName.trim() : "";
|
|
130
|
+
const goalSummary = typeof summary.goalSummary === "string" ? summary.goalSummary.trim() : "";
|
|
131
|
+
const finalState = typeof summary.finalState === "string" ? summary.finalState.trim() : "";
|
|
132
|
+
const notesForScript = typeof summary.notesForScript === "string" ? summary.notesForScript.trim() : "";
|
|
133
|
+
return `AUTHORING MODE:
|
|
134
|
+
Test design exploration is complete. Use the validated completion summary below and the conversation so far to produce the final test script now.
|
|
135
|
+
|
|
136
|
+
Do not call tools.
|
|
137
|
+
Do not continue exploring.
|
|
138
|
+
Do not add explanation.
|
|
139
|
+
Output only the final result in the required design-mode format.
|
|
140
|
+
|
|
141
|
+
Completion summary:
|
|
142
|
+
Suggested test name: ${suggestedTestName || "(choose a short kebab-case name)"}
|
|
143
|
+
Goal summary: ${goalSummary || "(not provided)"}
|
|
144
|
+
Final state: ${finalState || "(not provided)"}
|
|
145
|
+
Important steps:
|
|
146
|
+
${formatStringList(summary.importantSteps)}
|
|
147
|
+
Verification points:
|
|
148
|
+
${formatStringList(summary.verificationPoints)}
|
|
149
|
+
${notesForScript ? `Notes for script:\n${notesForScript}\n` : ""}`;
|
|
109
150
|
}
|
|
@@ -12,7 +12,7 @@ Apply the user's changes and output the revised test script.
|
|
|
12
12
|
FORMAT RULES:
|
|
13
13
|
- One simple instruction per line (NO numbers, NO bullets)
|
|
14
14
|
- Use imperative commands: "Open X", "Click Y", "Type Z"
|
|
15
|
-
-
|
|
15
|
+
- Use plain-language verification lines such as "Verify ..." or "Validate ..." for expected behavior
|
|
16
16
|
- End with "exit"
|
|
17
17
|
|
|
18
18
|
Output only the revised test script, nothing else.`;
|
|
@@ -39,11 +39,15 @@ CRITICAL RULES:
|
|
|
39
39
|
|
|
40
40
|
Your process:
|
|
41
41
|
1. Read the instruction
|
|
42
|
-
2.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
2. Choose the right tool when the instruction is really a higher-level capability:
|
|
43
|
+
- Use \`assert\` for assertions and verification intent, including legacy \`assert:\` lines and natural language like verify, validate, ensure, or confirm
|
|
44
|
+
- Use \`navigate\` for direct browser navigation instead of trying to manipulate the address bar
|
|
45
|
+
- Use \`loadmill\` only when the instruction explicitly references the word "loadmill"
|
|
46
|
+
3. Execute the required actions
|
|
47
|
+
4. Before tapping a mutating action, dismiss the keyboard if it is open and not needed
|
|
48
|
+
5. After a mutating action, inspect the resulting screen for success cues such as cleared fields, reset buttons, changed status, refreshed content, or navigation
|
|
49
|
+
6. Stop as soon as success is visible
|
|
50
|
+
7. Stop immediately - no commentary, no questions
|
|
47
51
|
|
|
48
52
|
Each instruction is independent. Do not reference previous instructions or ask about next steps.
|
|
49
53
|
${appContextSection ? `\n\n${appContextSection}` : ""}`;
|
|
@@ -191,7 +191,9 @@ export class CuaDebugTracer {
|
|
|
191
191
|
...(Number.isInteger(debugContext?.instructionIndex) ? { instructionIndex: debugContext.instructionIndex } : {})
|
|
192
192
|
};
|
|
193
193
|
const chainId = scope === "design" ? ids.sessionId : ids.runId;
|
|
194
|
-
const chainKey =
|
|
194
|
+
const chainKey = typeof debugContext?.chainId === "string" && debugContext.chainId.trim()
|
|
195
|
+
? debugContext.chainId.trim()
|
|
196
|
+
: `${scope}:${chainId || "unknown"}`;
|
|
195
197
|
const previousPendingSafetyChecks = Array.isArray(this.lastPendingSafetyChecksByChain.get(chainKey))
|
|
196
198
|
? this.lastPendingSafetyChecksByChain.get(chainKey)
|
|
197
199
|
: [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/droid-cua",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.1-rc.11",
|
|
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",
|
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
"test": "node --test unit-tests/*.test.js",
|
|
24
24
|
"build": "tsc",
|
|
25
25
|
"clean": "rm -rf build",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"postversion": "git push && git push --tags"
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
29
28
|
},
|
|
30
29
|
"repository": {
|
|
31
30
|
"type": "git",
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export const TARGET_SCALED_WIDTH = 400;
|
|
2
|
-
export const SCREENSHOT_RESOLUTION_MODE_DOWNSCALED = "downscaled";
|
|
3
|
-
export const SCREENSHOT_RESOLUTION_MODE_NATIVE = "native";
|
|
4
|
-
export function normalizeScreenshotResolutionMode(value) {
|
|
5
|
-
return value === SCREENSHOT_RESOLUTION_MODE_NATIVE
|
|
6
|
-
? SCREENSHOT_RESOLUTION_MODE_NATIVE
|
|
7
|
-
: SCREENSHOT_RESOLUTION_MODE_DOWNSCALED;
|
|
8
|
-
}
|
|
9
|
-
export function validateScreenshotResolutionMode(value, label) {
|
|
10
|
-
if (typeof value !== "string") {
|
|
11
|
-
throw new Error(`${label} must be one of: downscaled, native.`);
|
|
12
|
-
}
|
|
13
|
-
const normalized = normalizeScreenshotResolutionMode(value);
|
|
14
|
-
if (normalized !== value) {
|
|
15
|
-
throw new Error(`${label} must be one of: downscaled, native.`);
|
|
16
|
-
}
|
|
17
|
-
return normalized;
|
|
18
|
-
}
|
|
19
|
-
export function buildResolutionAwareDeviceInfo({ width, height, screenshotResolutionMode, }) {
|
|
20
|
-
const normalizedMode = normalizeScreenshotResolutionMode(screenshotResolutionMode);
|
|
21
|
-
const scale = normalizedMode === SCREENSHOT_RESOLUTION_MODE_NATIVE || width <= TARGET_SCALED_WIDTH
|
|
22
|
-
? 1.0
|
|
23
|
-
: TARGET_SCALED_WIDTH / width;
|
|
24
|
-
return {
|
|
25
|
-
scaled_width: Math.round(width * scale),
|
|
26
|
-
scaled_height: Math.round(height * scale),
|
|
27
|
-
scale,
|
|
28
|
-
screenshot_resolution_mode: normalizedMode,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export function readScreenshotResolutionModeFromEnv() {
|
|
32
|
-
return normalizeScreenshotResolutionMode(process.env.DROID_CUA_SCREENSHOT_RESOLUTION_MODE);
|
|
33
|
-
}
|